nolo-cli 0.1.22 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/agentRecordConfig.ts +4 -0
  3. package/agent-runtime/hostAdapter.ts +2 -0
  4. package/agent-runtime/hybridRecordStore.ts +30 -9
  5. package/agent-runtime/index.ts +7 -0
  6. package/agent-runtime/localLoop.ts +14 -4
  7. package/agent-runtime/localToolPolicy.ts +22 -10
  8. package/agent-runtime/localWorkspaceTools.ts +140 -374
  9. package/agent-runtime/platformChatProvider.ts +3 -0
  10. package/agent-runtime/runtimeToolPolicy.ts +133 -0
  11. package/agent-runtime/types.ts +47 -0
  12. package/agentAliases.ts +1 -1
  13. package/agentCommandSupport.ts +77 -0
  14. package/agentInternalCommandEntries.ts +34 -0
  15. package/agentListCommands.ts +123 -0
  16. package/agentListHelpers.ts +187 -0
  17. package/agentMachineCommands.ts +402 -0
  18. package/agentRecordCommands.ts +141 -0
  19. package/agentRecordHelpers.ts +395 -0
  20. package/agentRunCommand.ts +75 -3
  21. package/agentRuntimeCommands.ts +7 -773
  22. package/ai/agent/_executeModel.ts +29 -3
  23. package/ai/agent/buildSystemPrompt.ts +17 -0
  24. package/ai/agent/cliChatClient.ts +38 -4
  25. package/ai/agent/cliExecutor.ts +304 -25
  26. package/ai/agent/createAgentSchema.ts +2 -2
  27. package/ai/agent/guidedCreation/assistant.ts +82 -0
  28. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  29. package/ai/agent/guidedCreation/draft.ts +62 -0
  30. package/ai/agent/guidedCreation/types.ts +38 -0
  31. package/ai/agent/machineRunPermissions.ts +21 -3
  32. package/ai/agent/runtimeGuidance.ts +1 -1
  33. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  34. package/ai/agent/streamAgentChatTurn.ts +593 -36
  35. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  36. package/ai/ai.locale.ts +14 -2
  37. package/ai/chat/fetchUtils.native.ts +2 -0
  38. package/ai/chat/fetchUtils.ts +2 -0
  39. package/ai/chat/sendOpenAICompletionsRequest.ts +82 -1
  40. package/ai/chat/sendOpenAIResponseRequest.ts +65 -0
  41. package/ai/llm/getPricing.ts +14 -0
  42. package/ai/llm/kimi.ts +4 -1
  43. package/ai/llm/modelAvatar.ts +1 -0
  44. package/ai/llm/providers.ts +4 -1
  45. package/ai/llm/reasoningModels.ts +1 -0
  46. package/ai/taskRun/taskRunProtocol.ts +25 -10
  47. package/ai/token/modelUsageQuery.ts +17 -0
  48. package/ai/tools/agent/agentTools.ts +38 -0
  49. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  50. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  51. package/ai/tools/checkEnvTool.ts +52 -20
  52. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  53. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  54. package/ai/tools/index.ts +24 -14
  55. package/ai/tools/marxistsOfflineBook.ts +253 -0
  56. package/ai/tools/modelUsageTools.ts +9 -2
  57. package/ai/tools/readTool.ts +31 -0
  58. package/ai/tools/toolApiClient.ts +35 -1
  59. package/ai/tools/toolNameAliases.ts +13 -0
  60. package/cliCommandAdapters.ts +28 -0
  61. package/cliCommandDispatch.ts +66 -0
  62. package/cliCommandFactories.ts +34 -0
  63. package/cliCommandTypes.ts +49 -0
  64. package/cliEnvHelpers.ts +44 -0
  65. package/client/agentRun.test.ts +330 -24
  66. package/client/agentRun.ts +117 -35
  67. package/client/hybridRecordStore.test.ts +42 -0
  68. package/client/hybridRecordStore.ts +11 -0
  69. package/client/localRuntimeAdapter.test.ts +191 -21
  70. package/client/localRuntimeAdapter.ts +99 -10
  71. package/client/localRuntimeDryRun.test.ts +5 -2
  72. package/client/taskRunPrompt.ts +1 -0
  73. package/commandRegistry.ts +11 -61
  74. package/connector-experimental/capabilities.ts +1 -0
  75. package/database/actions/common.ts +4 -3
  76. package/database/config.ts +19 -0
  77. package/database/server/coreDataProxy.ts +43 -26
  78. package/index.ts +54 -191
  79. package/internalCommandEntries.ts +12 -0
  80. package/machineCommands.ts +688 -64
  81. package/offlineMarxistsAgentCommand.ts +314 -0
  82. package/package.json +22 -2
  83. package/render/canvas/canvasEditContext.ts +127 -0
  84. package/render/canvas/canvasRuntime.ts +57 -0
  85. package/render/canvas/canvasSnapshotParser.ts +76 -0
  86. package/render/canvas/canvasTree.ts +308 -0
  87. package/render/canvas/types.ts +46 -0
  88. package/render/layout/deleteBehavior.ts +52 -0
  89. package/render/layout/mainLayoutSidebar.ts +17 -0
  90. package/render/layout/mainLayoutViewMode.ts +56 -0
  91. package/render/layout/topbarUtils.ts +87 -0
  92. package/render/layout/useDevReloadPending.ts +30 -0
  93. package/render/page/createPageAction.ts +183 -0
  94. package/render/page/docSlice.ts +468 -0
  95. package/render/page/server/createPage.ts +174 -0
  96. package/render/page/server/handleCreatePage.ts +91 -0
  97. package/render/page/server/index.ts +4 -0
  98. package/render/page/types.ts +17 -0
  99. package/render/page/useKeyboardSave.ts +48 -0
  100. package/render/styles/zIndex.ts +12 -0
  101. package/render/surf/WeatherIconStyles.ts +17 -0
  102. package/render/surf/color.ts +9 -0
  103. package/render/surf/config.ts +46 -0
  104. package/render/surf/screens/style.ts +1 -0
  105. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  106. package/render/surf/utils/groupedWeatherData.ts +32 -0
  107. package/render/surf/weatherUtils.ts +50 -0
  108. package/render/table/activityColumns.ts +6 -0
  109. package/render/table/createTableAction.ts +270 -0
  110. package/render/table/deleteTableAction.ts +129 -0
  111. package/render/table/fetchAndCacheTableRows.ts +174 -0
  112. package/render/table/tableSlice.ts +1106 -0
  113. package/render/table/tableView.ts +289 -0
  114. package/render/table/toolValueUtils.ts +363 -0
  115. package/render/table/types.ts +252 -0
  116. package/render/table/useCreateTable.ts +72 -0
  117. package/render/table/useTable.ts +61 -0
  118. package/render/table/utils/tableSerialization.ts +50 -0
  119. package/render/web/elements/artifactPreviewCode.ts +43 -0
  120. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  121. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  122. package/render/web/elements/mermaidPreview.ts +21 -0
  123. package/render/web/ui/useInlineEdit.ts +135 -0
  124. package/runtimeDoctorCommands.ts +118 -0
  125. package/scriptCommandEntries.ts +71 -0
  126. package/systemInternalCommandEntries.ts +71 -0
  127. package/tableCommands.ts +42 -5
  128. package/workflowInternalCommandEntries.ts +11 -0
@@ -0,0 +1,217 @@
1
+ import React from "react";
2
+ import {
3
+ buildAgentFormDataFromGuidedDraft,
4
+ mergeGuidedAgentDraft,
5
+ validateGuidedAgentDraft,
6
+ } from "ai/agent/guidedCreation/draft";
7
+ import { DEFAULT_MODEL } from "ai/llm/providers";
8
+ import type {
9
+ GuidedAgentAssistantMessage,
10
+ GuidedAgentCapabilityId,
11
+ GuidedAgentDraft,
12
+ } from "ai/agent/guidedCreation/types";
13
+ import { mapCapabilityIdsToToolIds } from "ai/agent/guidedCreation/capabilities";
14
+
15
+ const EMPTY_DRAFT: GuidedAgentDraft = {
16
+ name: "",
17
+ introduction: "",
18
+ prompt: "",
19
+ promptSummary: "",
20
+ provider: DEFAULT_MODEL.provider,
21
+ model: DEFAULT_MODEL.name,
22
+ isPublic: false,
23
+ capabilityIds: [],
24
+ toolIds: [],
25
+ references: [],
26
+ tags: [],
27
+ unresolved: ["用途", "回答风格", "能力"],
28
+ };
29
+
30
+ const QUESTIONS = [
31
+ "你想让这个 AI 主要帮你完成什么?",
32
+ "它主要服务谁?希望用什么语气和风格回答?",
33
+ "它需要哪些能力:联网搜索、读写文档、处理表格、调用其他 AI、创建应用或代码?",
34
+ "有哪些资料、边界或禁区需要它遵守?",
35
+ "这个 AI 是只给自己用,还是公开给别人使用?",
36
+ ];
37
+
38
+ const textIncludes = (text: string, words: string[]) =>
39
+ words.some((word) => text.includes(word));
40
+
41
+ const inferCapabilityIds = (text: string): GuidedAgentCapabilityId[] => {
42
+ const lower = text.toLowerCase();
43
+ const ids: GuidedAgentCapabilityId[] = [];
44
+ if (textIncludes(lower, ["搜索", "联网", "网页", "资料", "最新", "research", "search"])) {
45
+ ids.push("webSearch");
46
+ }
47
+ if (textIncludes(lower, ["文档", "知识库", "页面", "资料", "doc", "docs", "knowledge"])) {
48
+ ids.push("docs");
49
+ }
50
+ if (textIncludes(lower, ["表格", "数据", "记录", "table", "sheet", "csv"])) {
51
+ ids.push("tables");
52
+ }
53
+ if (textIncludes(lower, ["多 agent", "多agent", "评审", "协作", "agent"])) {
54
+ ids.push("agents");
55
+ }
56
+ if (textIncludes(lower, ["应用", "代码", "app", "code", "开发", "生成页面"])) {
57
+ ids.push("apps");
58
+ }
59
+ return Array.from(new Set(ids));
60
+ };
61
+
62
+ const inferName = (text: string) => {
63
+ const cleaned = text
64
+ .replace(/[。!?!?,,;;::]/g, " ")
65
+ .replace(/\bAI\b/gi, "")
66
+ .replace(/帮我创建|创建一个|创建|面向/g, "")
67
+ .replace(/\s+/g, " ")
68
+ .trim();
69
+ if (!cleaned) return "";
70
+ const purpose = cleaned.slice(0, 18);
71
+ return `${purpose.replace(/\s+/g, "")}助手`;
72
+ };
73
+
74
+ const buildPrompt = (messages: GuidedAgentAssistantMessage[], draft: GuidedAgentDraft) => {
75
+ const userNeeds = messages
76
+ .filter((message) => message.role === "user")
77
+ .map((message) => `- ${message.content}`)
78
+ .join("\n");
79
+ return [
80
+ `你是${draft.name || "用户的专属 AI"}。`,
81
+ "你需要先理解用户目标,再给出可执行、清晰、有边界的帮助。",
82
+ "根据以下需求工作:",
83
+ userNeeds || "- 尚未补充具体需求",
84
+ "如果信息不足,先提出必要澄清问题;不要编造用户没有确认的资料来源。",
85
+ ].join("\n");
86
+ };
87
+
88
+ const buildNextQuestion = (draft: GuidedAgentDraft) => {
89
+ if (!draft.name || !draft.prompt) return QUESTIONS[1];
90
+ if (draft.capabilityIds.length === 0) return QUESTIONS[2];
91
+ if (!draft.introduction) return QUESTIONS[3];
92
+ if (draft.unresolved.includes("公开状态")) return QUESTIONS[4];
93
+ return "配置草稿已经可创建。你还想补充哪些资料、语气或使用边界?";
94
+ };
95
+
96
+ const createAssistantDraft = (
97
+ messages: GuidedAgentAssistantMessage[],
98
+ current: GuidedAgentDraft
99
+ ) => {
100
+ const userText = messages
101
+ .filter((message) => message.role === "user")
102
+ .map((message) => message.content)
103
+ .join("\n");
104
+ const capabilityIds = Array.from(
105
+ new Set([...current.capabilityIds, ...inferCapabilityIds(userText)])
106
+ );
107
+ const name = current.name || inferName(userText);
108
+ const isPublic =
109
+ current.isPublic ||
110
+ textIncludes(userText.toLowerCase(), ["公开", "分享", "public", "给别人"]);
111
+ const unresolved = [
112
+ !name ? "name" : "",
113
+ !userText.trim() ? "prompt" : "",
114
+ capabilityIds.length === 0 ? "能力" : "",
115
+ current.introduction || userText.length > 24 ? "" : "introduction",
116
+ textIncludes(userText.toLowerCase(), ["公开", "私有", "private", "public"]) ? "" : "公开状态",
117
+ ].filter(Boolean);
118
+ const next: GuidedAgentDraft = mergeGuidedAgentDraft(current, {
119
+ name,
120
+ introduction:
121
+ current.introduction ||
122
+ (userText.trim()
123
+ ? `面向你的需求,帮助整理、判断并执行:${userText.trim().slice(0, 70)}`
124
+ : ""),
125
+ promptSummary: userText.trim()
126
+ ? `围绕“${userText.trim().slice(0, 46)}”提供结构化帮助,必要时先追问。`
127
+ : "",
128
+ capabilityIds,
129
+ toolIds: mapCapabilityIdsToToolIds(capabilityIds),
130
+ isPublic,
131
+ tags: Array.from(new Set([...current.tags, ...capabilityIds])),
132
+ unresolved,
133
+ });
134
+ return mergeGuidedAgentDraft(next, {
135
+ prompt: buildPrompt(messages, next),
136
+ });
137
+ };
138
+
139
+ const createId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`;
140
+
141
+ export const useGuidedAgentCreation = (initialDraft?: Partial<GuidedAgentDraft> | null) => {
142
+ const [draft, setDraft] = React.useState<GuidedAgentDraft>(() =>
143
+ initialDraft ? mergeGuidedAgentDraft(EMPTY_DRAFT, initialDraft) : EMPTY_DRAFT
144
+ );
145
+ const [messages, setMessages] = React.useState<GuidedAgentAssistantMessage[]>([
146
+ {
147
+ id: "assistant-initial",
148
+ role: "assistant",
149
+ content: initialDraft?.name
150
+ ? "我已经带入了当前对话整理出的草稿。你可以继续补充要求,或直接预览并创建。"
151
+ : QUESTIONS[0],
152
+ },
153
+ ]);
154
+ const [input, setInput] = React.useState("");
155
+ const [isThinking, setIsThinking] = React.useState(false);
156
+ const [error, setError] = React.useState<string | null>(null);
157
+
158
+ const submit = React.useCallback(async () => {
159
+ const content = input.trim();
160
+ if (!content || isThinking) return;
161
+
162
+ setInput("");
163
+ setError(null);
164
+ setIsThinking(true);
165
+
166
+ const userMessage: GuidedAgentAssistantMessage = {
167
+ id: createId(),
168
+ role: "user",
169
+ content,
170
+ };
171
+
172
+ try {
173
+ const nextMessages = [...messages, userMessage];
174
+ const nextDraft = createAssistantDraft(nextMessages, draft);
175
+ const assistantMessage: GuidedAgentAssistantMessage = {
176
+ id: createId(),
177
+ role: "assistant",
178
+ content: buildNextQuestion(nextDraft),
179
+ };
180
+ setMessages([...nextMessages, assistantMessage]);
181
+ setDraft(nextDraft);
182
+ } catch {
183
+ setError("暂时没能整理这段需求,请换一种说法再试。");
184
+ } finally {
185
+ setIsThinking(false);
186
+ }
187
+ }, [draft, input, isThinking, messages]);
188
+
189
+ const toggleReference = React.useCallback((dbKey: string, selected: boolean) => {
190
+ setDraft((current) =>
191
+ mergeGuidedAgentDraft(current, {
192
+ references: current.references.map((reference) =>
193
+ reference.dbKey === dbKey ? { ...reference, selected } : reference
194
+ ),
195
+ })
196
+ );
197
+ }, []);
198
+
199
+ const validation = validateGuidedAgentDraft(draft);
200
+ const formInitialValues = React.useMemo(
201
+ () => buildAgentFormDataFromGuidedDraft(draft),
202
+ [draft]
203
+ );
204
+
205
+ return {
206
+ draft,
207
+ messages,
208
+ input,
209
+ setInput,
210
+ submit,
211
+ isThinking,
212
+ error,
213
+ validation,
214
+ formInitialValues,
215
+ toggleReference,
216
+ };
217
+ };
package/ai/ai.locale.ts CHANGED
@@ -27,6 +27,9 @@ export default {
27
27
  textOnly: "Text-only",
28
28
  details: "Details",
29
29
  price: "Price",
30
+ modelCost: "Model cost",
31
+ inputCostPerMillionTokens: "Input",
32
+ outputCostPerMillionTokens: "Output",
30
33
  referencePricePerTurn: "Per-turn reference price",
31
34
  defaultImageProfileEstimate: "Default profile estimate",
32
35
  perMillionTokens: "per 1M tokens",
@@ -251,7 +254,7 @@ export default {
251
254
  description: "Create a new Agent (intelligent assistant/app) based on the given configuration and return the complete Agent information.",
252
255
  params: {
253
256
  name: "The name of the Agent, e.g., 'Meeting Minutes Assistant'.",
254
- model: "The model name to use. Logic: Default 'xiaomi/mimo-v2.5-pro'. Complex tasks: 'openai/gpt-5.2'. Cost-saving: 'openai/gpt-5.1'. Aesthetic/Creative: 'gemini-3-pro-preview'. Long Context/Fast: 'x-ai/grok-4.1-fast'. Image Generation: 'gemini-3-pro-image-preview' (Best/Expensive) or 'gemini-2.5-flash-image' (Ordinary).",
257
+ model: "The model name to use. Logic: Default 'xiaomi/mimo-v2.5-pro'. Complex tasks: 'openai/gpt-5.2'. Cost-saving: 'openai/gpt-5.1'. Aesthetic/Creative: 'gemini-3-pro-preview'. Reliable low-cost summary/reasoning: 'deepseek-v4-pro'. Image Generation: 'gemini-3-pro-image-preview' (Best/Expensive) or 'gemini-2.5-flash-image' (Ordinary).",
255
258
  provider: "The model provider identifier, must match the selected model, e.g., 'openrouter', 'deepseek', 'fireworks', 'mistral', etc.",
256
259
  prompt: "System prompt/instruction describing the Agent's role, tone, and usage.",
257
260
  introduction: "Introduction displayed in the app market or list, explanatory text for end users.",
@@ -318,6 +321,9 @@ export default {
318
321
  textOnly: "纯文本",
319
322
  details: "详细信息",
320
323
  price: "价格",
324
+ modelCost: "模型成本",
325
+ inputCostPerMillionTokens: "输入",
326
+ outputCostPerMillionTokens: "输出",
321
327
  referencePricePerTurn: "按次参考价",
322
328
  defaultImageProfileEstimate: "默认档参考价",
323
329
  perMillionTokens: "每百万Token",
@@ -539,7 +545,7 @@ export default {
539
545
  description: "根据给定配置创建一个新的 Agent(智能体 / 应用),并返回完整的 Agent 信息。",
540
546
  params: {
541
547
  name: "Agent 的名称,例如:'会议纪要助手'。",
542
- model: "默认使用 'xiaomi/mimo-v2.5-pro'。复杂任务: 'openai/gpt-5.2'。省成本但复杂: 'openai/gpt-5.1'。追求美观/创意: 'gemini-3-pro-preview'。长上下文/快速: 'x-ai/grok-4.1-fast'。生成图片: 贵/高质量选 'gemini-3-pro-image-preview',普通选 'gemini-2.5-flash-image'。",
548
+ model: "默认使用 'xiaomi/mimo-v2.5-pro'。复杂任务: 'openai/gpt-5.2'。省成本但复杂: 'openai/gpt-5.1'。追求美观/创意: 'gemini-3-pro-preview'。低成本摘要/推理: 'deepseek-v4-pro'。生成图片: 贵/高质量选 'gemini-3-pro-image-preview',普通选 'gemini-2.5-flash-image'。",
543
549
  provider: "模型提供方标识,必须与所选模型相匹配,例如 'openrouter'、'deepseek'、'fireworks'、'mistral' 等。",
544
550
  prompt: "系统提示词 / 指令,描述这个 Agent 的角色、语气和使用方式。",
545
551
  introduction: "在应用市场或列表中展示的简介,面向终端用户的说明文案。",
@@ -606,6 +612,9 @@ export default {
606
612
  textOnly: "純文字",
607
613
  details: "詳細資訊",
608
614
  price: "價格",
615
+ modelCost: "模型成本",
616
+ inputCostPerMillionTokens: "輸入",
617
+ outputCostPerMillionTokens: "輸出",
609
618
  referencePricePerTurn: "按次參考價",
610
619
  defaultImageProfileEstimate: "預設檔參考價",
611
620
  perMillionTokens: "每百萬Token",
@@ -852,6 +861,9 @@ export default {
852
861
  textOnly: "テキストのみ",
853
862
  details: "詳細",
854
863
  price: "価格",
864
+ modelCost: "モデルコスト",
865
+ inputCostPerMillionTokens: "入力",
866
+ outputCostPerMillionTokens: "出力",
855
867
  referencePricePerTurn: "回ごとの参考価格",
856
868
  defaultImageProfileEstimate: "デフォルト構成の参考価格",
857
869
  perMillionTokens: "100万トークンあたり",
@@ -42,7 +42,9 @@ const buildProxyPayload = (
42
42
  ...bodyData,
43
43
  url: api,
44
44
  provider,
45
+ agentKey: agentConfig.dbKey,
45
46
  ...(apiSource ? { apiSource } : {}),
47
+ ...((agentConfig as any).apiKeyHeader ? { apiKeyHeader: (agentConfig as any).apiKeyHeader } : {}),
46
48
  KEY: apiKey,
47
49
  };
48
50
  };
@@ -40,7 +40,9 @@ const buildProxyPayload = (
40
40
  ...bodyData,
41
41
  url: api,
42
42
  provider,
43
+ agentKey: agentConfig.dbKey,
43
44
  ...(apiSource ? { apiSource } : {}),
45
+ ...((agentConfig as any).apiKeyHeader ? { apiKeyHeader: (agentConfig as any).apiKeyHeader } : {}),
44
46
  KEY: apiKey,
45
47
  };
46
48
  };
@@ -160,6 +160,23 @@ export type CompletionMeta = {
160
160
  usage?: any;
161
161
  };
162
162
 
163
+ const STREAM_READ_TIMEOUT_MS = 45_000;
164
+
165
+ const logQuickChatPerfStage = (
166
+ startedAt: number | undefined,
167
+ stage: string,
168
+ details?: Record<string, unknown>
169
+ ) => {
170
+ if (!startedAt) return;
171
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
172
+ console.info("[QuickChatPerf]", {
173
+ stage,
174
+ elapsedMs: now - startedAt,
175
+ ...(typeof performance !== "undefined" ? { atMs: now } : {}),
176
+ ...(details ?? {}),
177
+ });
178
+ };
179
+
163
180
  /**
164
181
  * 初始化流式状态
165
182
  */
@@ -398,6 +415,28 @@ async function processAccumulatedToolCalls(
398
415
  };
399
416
  }
400
417
 
418
+ async function readStreamChunkWithTimeout(
419
+ reader: ReadableStreamDefaultReader<Uint8Array>
420
+ ) {
421
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
422
+ try {
423
+ return await Promise.race([
424
+ reader.read(),
425
+ new Promise<ReadableStreamReadResult<Uint8Array>>((_, reject) => {
426
+ timeoutId = setTimeout(() => {
427
+ reject(
428
+ new Error(
429
+ `模型响应流 ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)} 秒内没有返回新内容`
430
+ )
431
+ );
432
+ }, STREAM_READ_TIMEOUT_MS);
433
+ }),
434
+ ]);
435
+ } finally {
436
+ if (timeoutId) clearTimeout(timeoutId);
437
+ }
438
+ }
439
+
401
440
  /**
402
441
  * 处理「流结束」场景(done === true)
403
442
  */
@@ -455,6 +494,7 @@ export const sendOpenAICompletionsRequest = async ({
455
494
  parentMessageId,
456
495
  messageMetadata,
457
496
  disableToolsForThisRequest = false,
497
+ quickChatPerfStartedAt,
458
498
  }: {
459
499
  bodyData: any;
460
500
  agentConfig: any;
@@ -463,6 +503,7 @@ export const sendOpenAICompletionsRequest = async ({
463
503
  parentMessageId?: string;
464
504
  messageMetadata?: Partial<Message>;
465
505
  disableToolsForThisRequest?: boolean;
506
+ quickChatPerfStartedAt?: number;
466
507
  }): Promise<CompletionMeta> => {
467
508
  const { dispatch, getState, signal: thunkSignal } = thunkApi;
468
509
 
@@ -549,6 +590,10 @@ export const sendOpenAICompletionsRequest = async ({
549
590
 
550
591
  const api = getApiEndpoint(agentConfig);
551
592
  const token = selectCurrentToken(getState() as RootState);
593
+ logQuickChatPerfStage(quickChatPerfStartedAt, "openai-completions-fetch-starting", {
594
+ api,
595
+ dialogKey,
596
+ });
552
597
  const response = await performFetchRequest({
553
598
  agentConfig,
554
599
  api,
@@ -557,6 +602,11 @@ export const sendOpenAICompletionsRequest = async ({
557
602
  signal,
558
603
  token,
559
604
  });
605
+ logQuickChatPerfStage(quickChatPerfStartedAt, "openai-completions-fetch-response", {
606
+ ok: response.ok,
607
+ status: response.status,
608
+ dialogKey,
609
+ });
560
610
 
561
611
  if (!response.ok) {
562
612
  const errorMessage = await parseApiError(response);
@@ -578,9 +628,12 @@ export const sendOpenAICompletionsRequest = async ({
578
628
  }
579
629
 
580
630
  const decoder = new TextDecoder();
631
+ let loggedFirstStreamChunk = false;
632
+ let loggedFirstParsedEvent = false;
633
+ let loggedFirstVisibleDelta = false;
581
634
 
582
635
  while (true) {
583
- const { done, value } = await reader.read();
636
+ const { done, value } = await readStreamChunkWithTimeout(reader);
584
637
 
585
638
  if (done) {
586
639
  const completion = await handleStreamCompletion(
@@ -602,8 +655,25 @@ export const sendOpenAICompletionsRequest = async ({
602
655
  break;
603
656
  }
604
657
 
658
+ if (!loggedFirstStreamChunk) {
659
+ loggedFirstStreamChunk = true;
660
+ logQuickChatPerfStage(
661
+ quickChatPerfStartedAt,
662
+ "openai-completions-first-stream-chunk",
663
+ { dialogKey, byteLength: value.byteLength }
664
+ );
665
+ }
666
+
605
667
  const chunk = decoder.decode(value, { stream: true });
606
668
  const parsedResults = parseSSE(chunk);
669
+ if (parsedResults.length > 0 && !loggedFirstParsedEvent) {
670
+ loggedFirstParsedEvent = true;
671
+ logQuickChatPerfStage(
672
+ quickChatPerfStartedAt,
673
+ "openai-completions-first-sse-event",
674
+ { dialogKey, eventCount: parsedResults.length }
675
+ );
676
+ }
607
677
 
608
678
  for (const parsedData of parsedResults) {
609
679
  const dataList = Array.isArray(parsedData) ? parsedData : [parsedData];
@@ -641,6 +711,14 @@ export const sendOpenAICompletionsRequest = async ({
641
711
  delta
642
712
  );
643
713
  streamState = updatedState;
714
+ if (hasNewVisibleContent && !loggedFirstVisibleDelta) {
715
+ loggedFirstVisibleDelta = true;
716
+ logQuickChatPerfStage(
717
+ quickChatPerfStartedAt,
718
+ "openai-completions-first-visible-delta",
719
+ { dialogKey }
720
+ );
721
+ }
644
722
 
645
723
  emitStreamingUpdate(hasNewVisibleContent, streamState, {
646
724
  dispatch,
@@ -712,6 +790,9 @@ export const sendOpenAICompletionsRequest = async ({
712
790
  };
713
791
  streamState = await finalizeStream(streamState, finalizeCtx);
714
792
  } finally {
793
+ logQuickChatPerfStage(quickChatPerfStartedAt, "openai-completions-stream-finished", {
794
+ dialogKey,
795
+ });
715
796
  dispatch(removeActiveController({ messageId, dialogKey }));
716
797
  try {
717
798
  await reader?.cancel();
@@ -42,6 +42,21 @@ const shouldEnableBuiltInImageGeneration = (agentConfig: any): boolean =>
42
42
  !getModelInfo(String(agentConfig?.model || ""))?.hasImageOutput &&
43
43
  !!agentConfig?.imageConfig?.enabled;
44
44
 
45
+ const logQuickChatPerfStage = (
46
+ startedAt: number | undefined,
47
+ stage: string,
48
+ details?: Record<string, unknown>
49
+ ) => {
50
+ if (!startedAt) return;
51
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
52
+ console.info("[QuickChatPerf]", {
53
+ stage,
54
+ elapsedMs: now - startedAt,
55
+ ...(typeof performance !== "undefined" ? { atMs: now } : {}),
56
+ ...(details ?? {}),
57
+ });
58
+ };
59
+
45
60
  type StreamState = {
46
61
  content: string;
47
62
  contentBuffer: Array<
@@ -140,6 +155,7 @@ export const sendOpenAIResponseRequest = async ({
140
155
  dialogKey,
141
156
  parentMessageId,
142
157
  messageMetadata,
158
+ quickChatPerfStartedAt,
143
159
  }: {
144
160
  bodyData: any;
145
161
  agentConfig: any;
@@ -147,6 +163,7 @@ export const sendOpenAIResponseRequest = async ({
147
163
  dialogKey: string;
148
164
  parentMessageId?: string;
149
165
  messageMetadata?: Partial<Message>;
166
+ quickChatPerfStartedAt?: number;
150
167
  }): Promise<CompletionMeta> => {
151
168
  const { dispatch, getState, signal: thunkSignal } = thunkApi;
152
169
  const dialogId = extractCustomId(dialogKey);
@@ -354,6 +371,10 @@ export const sendOpenAIResponseRequest = async ({
354
371
 
355
372
  const api = getApiEndpoint(agentConfig);
356
373
  const token = selectCurrentToken(getState() as RootState);
374
+ logQuickChatPerfStage(quickChatPerfStartedAt, "openai-response-fetch-starting", {
375
+ api,
376
+ dialogKey,
377
+ });
357
378
  const response = await performFetchRequest({
358
379
  agentConfig,
359
380
  api,
@@ -362,6 +383,11 @@ export const sendOpenAIResponseRequest = async ({
362
383
  signal,
363
384
  token,
364
385
  });
386
+ logQuickChatPerfStage(quickChatPerfStartedAt, "openai-response-fetch-response", {
387
+ ok: response.ok,
388
+ status: response.status,
389
+ dialogKey,
390
+ });
365
391
 
366
392
  if (!response.ok) {
367
393
  const errorMessage = await parseApiError(response);
@@ -379,13 +405,33 @@ export const sendOpenAIResponseRequest = async ({
379
405
  const parseSSE = createSSEParser();
380
406
  const decoder = new TextDecoder();
381
407
  let finishReason: string | null = null;
408
+ let loggedFirstStreamChunk = false;
409
+ let loggedFirstParsedEvent = false;
410
+ let loggedFirstVisibleDelta = false;
382
411
 
383
412
  while (true) {
384
413
  const { done, value } = await reader.read();
385
414
  if (done) break;
386
415
 
416
+ if (!loggedFirstStreamChunk) {
417
+ loggedFirstStreamChunk = true;
418
+ logQuickChatPerfStage(
419
+ quickChatPerfStartedAt,
420
+ "openai-response-first-stream-chunk",
421
+ { dialogKey, byteLength: value.byteLength }
422
+ );
423
+ }
424
+
387
425
  const chunk = decoder.decode(value, { stream: true });
388
426
  const events = parseSSE(chunk);
427
+ if (events.length > 0 && !loggedFirstParsedEvent) {
428
+ loggedFirstParsedEvent = true;
429
+ logQuickChatPerfStage(
430
+ quickChatPerfStartedAt,
431
+ "openai-response-first-sse-event",
432
+ { dialogKey, eventCount: events.length }
433
+ );
434
+ }
389
435
  const eventList = Array.isArray(events) ? events : [events];
390
436
 
391
437
  for (const event of eventList) {
@@ -404,6 +450,14 @@ export const sendOpenAIResponseRequest = async ({
404
450
  if (event.delta) {
405
451
  state.content += event.delta;
406
452
  state.contentBuffer = seg(state.content);
453
+ if (!loggedFirstVisibleDelta) {
454
+ loggedFirstVisibleDelta = true;
455
+ logQuickChatPerfStage(
456
+ quickChatPerfStartedAt,
457
+ "openai-response-first-visible-delta",
458
+ { dialogKey }
459
+ );
460
+ }
407
461
  flush();
408
462
  }
409
463
  break;
@@ -427,6 +481,14 @@ export const sendOpenAIResponseRequest = async ({
427
481
  if (itemText) {
428
482
  state.content = itemText;
429
483
  state.contentBuffer = seg(state.content);
484
+ if (!loggedFirstVisibleDelta) {
485
+ loggedFirstVisibleDelta = true;
486
+ logQuickChatPerfStage(
487
+ quickChatPerfStartedAt,
488
+ "openai-response-first-visible-delta",
489
+ { dialogKey }
490
+ );
491
+ }
430
492
  flush();
431
493
  }
432
494
  }
@@ -506,6 +568,9 @@ export const sendOpenAIResponseRequest = async ({
506
568
  await finalize();
507
569
  return buildMeta(false, false, "error");
508
570
  } finally {
571
+ logQuickChatPerfStage(quickChatPerfStartedAt, "openai-response-stream-finished", {
572
+ dialogKey,
573
+ });
509
574
  dispatch(removeActiveController({ messageId, dialogKey }));
510
575
  await safeCancel(reader);
511
576
  }
@@ -209,6 +209,20 @@ export const formatPriceAmount = (amount: number): string => {
209
209
  return amount.toFixed(6);
210
210
  };
211
211
 
212
+ export const formatModelCostPerMillion = (
213
+ value?: number,
214
+ creditsUnit = "积分"
215
+ ): string => {
216
+ if (typeof value !== "number" || !Number.isFinite(value)) return "未知";
217
+ if (value === 0) return "免费";
218
+ const decimals = Math.abs(value) < 1 ? 3 : 2;
219
+ const amount = value
220
+ .toFixed(decimals)
221
+ .replace(/\.0+$/, "")
222
+ .replace(/(\.\d*?[1-9])0+$/, "$1");
223
+ return `${amount} ${creditsUnit}`;
224
+ };
225
+
212
226
  export const formatCompactTurnPrice = (
213
227
  amount: number
214
228
  ): CompactTurnPriceDisplay => {
package/ai/llm/kimi.ts CHANGED
@@ -2,11 +2,14 @@ export const FIREWORKS_KIMI_LATEST_MODEL = "accounts/fireworks/models/kimi-lates
2
2
  export const FIREWORKS_KIMI_CURRENT_MODEL = "accounts/fireworks/models/kimi-k2p6";
3
3
  export const DEEPINFRA_KIMI_FALLBACK_MODEL = "moonshotai/Kimi-K2.6";
4
4
  export const OPENROUTER_KIMI_FALLBACK_MODEL = "moonshotai/kimi-k2.6";
5
- export const KIMI_PLATFORM_FALLBACK_STATUSES = [402, 429, 500, 502, 503, 504];
5
+ export const KIMI_PLATFORM_FALLBACK_STATUSES = [401, 402, 429, 500, 502, 503, 504];
6
6
 
7
7
  export const isFireworksKimiModel = (model?: string | null): boolean =>
8
8
  model === FIREWORKS_KIMI_LATEST_MODEL || model === FIREWORKS_KIMI_CURRENT_MODEL;
9
9
 
10
+ export const isDeepInfraKimiModel = (model?: string | null): boolean =>
11
+ model === DEEPINFRA_KIMI_FALLBACK_MODEL;
12
+
10
13
  export const resolveFireworksKimiModel = (model?: string | null): string => {
11
14
  if (model === FIREWORKS_KIMI_LATEST_MODEL) {
12
15
  return FIREWORKS_KIMI_CURRENT_MODEL;
@@ -393,6 +393,7 @@ export async function getModelAvatarComponent(
393
393
  if (cliProviderKey === "copilot") return get("copilot");
394
394
  if (cliProviderKey === "codex") return get("openai");
395
395
  if (cliProviderKey === "claude") return get("claude");
396
+ if (cliProviderKey === "agy") return get("gemini");
396
397
 
397
398
  // ── 按 model 名称匹配(优先,处理 openrouter/deepinfra 等聚合场景) ──
398
399
  if (m.includes("claude")) return get("claude");
@@ -267,7 +267,7 @@ export function getApiEndpoint(agent: Agent): string {
267
267
  }
268
268
 
269
269
  /* custom provider / apiSource=custom 但未给 URL 的兜底
270
- * 注意:apiSource="custom" 的 agent(如 kimi-k2.5)不需要 provider 字段,
270
+ * 注意:apiSource="custom" 的 agent 不需要 provider 字段,
271
271
  * 使用 customProviderUrl + 自己的 apiKey,上面已经 return 了。
272
272
  * 只有 customProviderUrl 为空时才会走到这里报错。
273
273
  */
@@ -276,6 +276,9 @@ export function getApiEndpoint(agent: Agent): string {
276
276
  effectiveProvider.toLowerCase() === "custom" ||
277
277
  (agent as any).apiSource === "custom"
278
278
  ) {
279
+ if (agent.useServerProxy) {
280
+ return "";
281
+ }
279
282
  throw new Error(
280
283
  "Custom provider URL is required when apiSource is 'custom'."
281
284
  );
@@ -4,6 +4,7 @@ const REASONING_MODEL_NAMES = new Set([
4
4
  "deepseek-reasoner",
5
5
  "gemini-2.5-pro",
6
6
  "gemini-2.5-flash",
7
+ "gemini-3.5-flash",
7
8
  "gemini-3-flash-preview",
8
9
  "gemini-3-pro-preview",
9
10
  "gemini-3.1-pro-preview",