refactor: 重命名项目名称从"体己"到"康康" 将整个项目的目录结构从"体己"重命名为"康康",包括所有源代码文件、 资源文件、测试文件以及Xcode项目配置文件。此更改涉及项目中所有的 文件路径和应用入口点(App/TijiApp.swift → App/KangkangApp.swift)。 ```
204 lines
7.0 KiB
Swift
204 lines
7.0 KiB
Swift
#if DEBUG
|
|
import SwiftUI
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
|
|
/// DEBUG 自检:加载 LLM 并跑一段 prompt,流式显示 token + 速率。
|
|
/// 同时显示沙盒 Application Support 路径,方便把模型拷进去。
|
|
struct DebugAIRunner: View {
|
|
@State private var output: String = ""
|
|
@State private var status: String = "未开始"
|
|
@State private var rate: Double = 0
|
|
@State private var running = false
|
|
@State private var modelReady: Bool = false
|
|
@State private var importingModel = false
|
|
@State private var importError: String?
|
|
|
|
private var appSupportPath: String {
|
|
(try? FileManager.default.url(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask,
|
|
appropriateFor: nil,
|
|
create: false
|
|
).path) ?? "(无法获取)"
|
|
}
|
|
|
|
private var modelExpectedPath: String {
|
|
appSupportPath + "/Models/Qwen3-1.7B-4bit"
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
Text("DEBUG · AI 自检")
|
|
.font(.system(size: 15, weight: .semibold))
|
|
.foregroundStyle(Tj.Palette.text)
|
|
|
|
// 沙盒路径与模型状态卡
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("模型预期路径")
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
Text(modelExpectedPath)
|
|
.font(.system(size: 10, design: .monospaced))
|
|
.foregroundStyle(Tj.Palette.text2)
|
|
.textSelection(.enabled)
|
|
.lineLimit(3)
|
|
HStack(spacing: 8) {
|
|
Button("复制路径") {
|
|
UIPasteboard.general.string = modelExpectedPath
|
|
}
|
|
.font(.system(size: 11))
|
|
.buttonStyle(.borderless)
|
|
|
|
Button(importingModel ? "导入中..." : "导入模型") {
|
|
importingModel = true
|
|
}
|
|
.font(.system(size: 11))
|
|
.buttonStyle(.borderless)
|
|
.disabled(importingModel)
|
|
|
|
Spacer()
|
|
|
|
Text(modelReady ? "✓ 模型就绪" : "⚠ 模型未就绪")
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundStyle(modelReady ? Tj.Palette.leaf : Tj.Palette.brick)
|
|
}
|
|
if let importError {
|
|
Text(importError)
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(Tj.Palette.brick)
|
|
.lineLimit(3)
|
|
}
|
|
}
|
|
.padding(10)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(Color.black.opacity(0.03))
|
|
)
|
|
|
|
// 推理状态
|
|
HStack {
|
|
Text("状态:\(status)")
|
|
Spacer()
|
|
Text(String(format: "%.1f tok/s", rate))
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
.monospaced()
|
|
}
|
|
.font(.system(size: 12))
|
|
|
|
Button(running ? "推理中..." : "跑一段 prompt") {
|
|
Task { await run() }
|
|
}
|
|
.buttonStyle(TjPrimaryButton())
|
|
.disabled(running)
|
|
|
|
ScrollView {
|
|
Text(output.isEmpty ? "(暂无输出)" : output)
|
|
.font(.system(.footnote, design: .monospaced))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(10)
|
|
}
|
|
.frame(maxHeight: 240)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(Color.black.opacity(0.04))
|
|
)
|
|
}
|
|
.padding(16)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Tj.Radius.md)
|
|
.fill(Color.yellow.opacity(0.08))
|
|
)
|
|
.padding(.horizontal, 16)
|
|
.onAppear { refreshModelStatus() }
|
|
.fileImporter(
|
|
isPresented: $importingModel,
|
|
allowedContentTypes: [.folder],
|
|
allowsMultipleSelection: false
|
|
) { result in
|
|
importModelFolder(from: result)
|
|
}
|
|
}
|
|
|
|
private func refreshModelStatus() {
|
|
modelReady = ModelStore.shared.isReady(.llm)
|
|
}
|
|
|
|
private func importModelFolder(from result: Result<[URL], Error>) {
|
|
defer {
|
|
refreshModelStatus()
|
|
importingModel = false
|
|
}
|
|
|
|
do {
|
|
guard let pickedURL = try result.get().first else {
|
|
importError = "未选择模型文件夹"
|
|
return
|
|
}
|
|
|
|
let securityScoped = pickedURL.startAccessingSecurityScopedResource()
|
|
defer {
|
|
if securityScoped {
|
|
pickedURL.stopAccessingSecurityScopedResource()
|
|
}
|
|
}
|
|
|
|
let sourceURL = resolvedModelSourceURL(from: pickedURL)
|
|
guard FileManager.default.fileExists(
|
|
atPath: sourceURL.appendingPathComponent(ModelKind.llm.sentinelFilename).path
|
|
) else {
|
|
importError = "请选择包含 config.json 的 Qwen3-1.7B-4bit 文件夹"
|
|
return
|
|
}
|
|
|
|
let targetURL = ModelStore.shared.localURL(for: .llm)
|
|
let parentURL = targetURL.deletingLastPathComponent()
|
|
try FileManager.default.createDirectory(at: parentURL, withIntermediateDirectories: true)
|
|
if FileManager.default.fileExists(atPath: targetURL.path) {
|
|
try FileManager.default.removeItem(at: targetURL)
|
|
}
|
|
try FileManager.default.copyItem(at: sourceURL, to: targetURL)
|
|
importError = nil
|
|
} catch {
|
|
importError = "导入失败:\(error.localizedDescription)"
|
|
}
|
|
}
|
|
|
|
private func resolvedModelSourceURL(from pickedURL: URL) -> URL {
|
|
let nestedURL = pickedURL.appendingPathComponent(ModelKind.llm.rawValue, isDirectory: true)
|
|
if FileManager.default.fileExists(
|
|
atPath: nestedURL.appendingPathComponent(ModelKind.llm.sentinelFilename).path
|
|
) {
|
|
return nestedURL
|
|
}
|
|
return pickedURL
|
|
}
|
|
|
|
@MainActor
|
|
private func run() async {
|
|
running = true
|
|
output = ""
|
|
rate = 0
|
|
status = "加载模型..."
|
|
do {
|
|
try await AIRuntime.shared.prepare()
|
|
status = "推理中..."
|
|
|
|
let prompt = "用中文一句话介绍肝功能里 ALT 这个指标。"
|
|
for try await chunk in await AIRuntime.shared.generate(
|
|
prompt: prompt,
|
|
maxTokens: 200
|
|
) {
|
|
output += chunk.text
|
|
rate = chunk.decodeRate
|
|
}
|
|
status = "完成 ✓"
|
|
} catch {
|
|
status = "失败:\(error.localizedDescription)"
|
|
}
|
|
running = false
|
|
refreshModelStatus()
|
|
}
|
|
}
|
|
#endif
|