@coze-arch/cli 0.0.14 → 0.0.15-alpha.a0f5b9

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 (91) hide show
  1. package/lib/__templates__/nextjs/scripts/prepare.sh +3 -0
  2. package/lib/__templates__/nuxt-vue/scripts/prepare.sh +3 -0
  3. package/lib/__templates__/pi-agent/.coze +10 -0
  4. package/lib/__templates__/pi-agent/AGENTS.md +140 -0
  5. package/lib/__templates__/pi-agent/README.md +172 -0
  6. package/lib/__templates__/pi-agent/_gitignore +3 -0
  7. package/lib/__templates__/pi-agent/_npmrc +23 -0
  8. package/lib/__templates__/pi-agent/docs/project-overview.md +356 -0
  9. package/lib/__templates__/pi-agent/docs/user/getting-started.md +47 -0
  10. package/lib/__templates__/pi-agent/package.json +60 -0
  11. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  12. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  13. package/lib/__templates__/pi-agent/pi-resources/extensions/test-ping.ts +19 -0
  14. package/lib/__templates__/pi-agent/pi-resources/prompts/test-prompt.md +11 -0
  15. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +36 -0
  16. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +9 -0
  17. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +41 -0
  18. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +9 -0
  19. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +85 -0
  20. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +9 -0
  21. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +53 -0
  22. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +9 -0
  23. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8285 -0
  24. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  25. package/lib/__templates__/pi-agent/scripts/prepare.sh +2 -0
  26. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  27. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  28. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  29. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  30. package/lib/__templates__/pi-agent/src/config.ts +596 -0
  31. package/lib/__templates__/pi-agent/src/core.ts +218 -0
  32. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +148 -0
  33. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
  34. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +141 -0
  35. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  36. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  37. package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
  38. package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  65. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  66. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  67. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  68. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  69. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  70. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  71. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  72. package/lib/__templates__/pi-agent/template.config.js +45 -0
  73. package/lib/__templates__/pi-agent/tests/config.test.ts +315 -0
  74. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
  75. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  76. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  77. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  78. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  79. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  80. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  81. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  82. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  83. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  84. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  85. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  86. package/lib/__templates__/taro/pnpm-lock.yaml +24 -14
  87. package/lib/__templates__/taro/server/package.json +0 -2
  88. package/lib/__templates__/templates.json +24 -0
  89. package/lib/__templates__/vite/scripts/prepare.sh +3 -0
  90. package/lib/cli.js +106 -8
  91. package/package.json +1 -1
@@ -0,0 +1,315 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import test from "node:test";
6
+ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
7
+ import {
8
+ getDefaultConfigPath,
9
+ loadConfig,
10
+ loadBotAppConfig,
11
+ resolveRuntimeModel
12
+ } from "../src/config.js";
13
+
14
+ function createConfigFile(config: unknown): string {
15
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-bot-workspace-config-"));
16
+ const workspaceDir = join(tempDir, "workspace");
17
+ const configPath = join(workspaceDir, "config.json");
18
+
19
+ mkdirSync(workspaceDir, { recursive: true });
20
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
21
+
22
+ return configPath;
23
+ }
24
+
25
+ test("loadConfig resolves workspace directory and default model", () => {
26
+ const configPath = createConfigFile({
27
+ agents: {
28
+ defaults: {
29
+ workspace: ".",
30
+ thinkingLevel: "medium",
31
+ model: {
32
+ primary: "coze/glm-4-7-251222"
33
+ }
34
+ }
35
+ },
36
+ models: {
37
+ providers: {
38
+ coze: {
39
+ api: "openai-completions",
40
+ baseUrl: "${COZE_BASE_URL}",
41
+ apiKey: "${COZE_API_KEY}",
42
+ models: [
43
+ {
44
+ id: "glm-4-7-251222",
45
+ name: "GLM 4.7",
46
+ input: ["text"],
47
+ reasoning: false,
48
+ contextWindow: 200000,
49
+ maxTokens: 8192,
50
+ cost: {
51
+ input: 0,
52
+ output: 0,
53
+ cacheRead: 0,
54
+ cacheWrite: 0
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ });
62
+
63
+ const loaded = loadConfig(configPath);
64
+ const tempRoot = dirname(dirname(configPath));
65
+
66
+ rmSync(tempRoot, { recursive: true, force: true });
67
+
68
+ assert.equal(loaded.workspaceDir, dirname(configPath));
69
+ assert.equal(loaded.thinkingLevel, "medium");
70
+ assert.deepEqual(loaded.defaultModel, {
71
+ provider: "coze",
72
+ modelId: "glm-4-7-251222"
73
+ });
74
+ });
75
+
76
+ test("loadBotAppConfig defaults agent mode to pi when env override is absent", () => {
77
+ const configPath = createConfigFile({
78
+ agents: {
79
+ defaults: {
80
+ workspace: ".",
81
+ model: {
82
+ primary: "openai/gpt-5-mini"
83
+ }
84
+ }
85
+ }
86
+ });
87
+
88
+ const loaded = loadBotAppConfig({
89
+ configPath,
90
+ env: {}
91
+ });
92
+ const tempRoot = dirname(dirname(configPath));
93
+
94
+ rmSync(tempRoot, { recursive: true, force: true });
95
+
96
+ assert.equal(loaded.agent.mode, "pi");
97
+ });
98
+
99
+ test("resolveRuntimeModel builds a custom provider model from workspace/config.json", async () => {
100
+ const configPath = createConfigFile({
101
+ agents: {
102
+ defaults: {
103
+ workspace: ".",
104
+ model: {
105
+ primary: "coze/glm-4-7-251222"
106
+ }
107
+ }
108
+ },
109
+ models: {
110
+ providers: {
111
+ missing: {
112
+ api: "openai-completions",
113
+ baseUrl: "${MISSING_BASE_URL}",
114
+ apiKey: "${MISSING_API_KEY}",
115
+ models: [
116
+ {
117
+ id: "unused-model",
118
+ name: "Unused",
119
+ input: ["text"],
120
+ reasoning: false
121
+ }
122
+ ]
123
+ },
124
+ coze: {
125
+ api: "openai-completions",
126
+ baseUrl: "${COZE_BASE_URL}",
127
+ apiKey: "${COZE_API_KEY}",
128
+ headers: {
129
+ "X-Workspace": "${WORKSPACE_ID}"
130
+ },
131
+ models: [
132
+ {
133
+ id: "glm-4-7-251222",
134
+ name: "GLM 4.7",
135
+ input: ["text"],
136
+ reasoning: false,
137
+ contextWindow: 200000,
138
+ maxTokens: 8192,
139
+ headers: {
140
+ "X-Trace": "${TRACE_ID}"
141
+ },
142
+ cost: {
143
+ input: 0,
144
+ output: 0,
145
+ cacheRead: 0,
146
+ cacheWrite: 0
147
+ }
148
+ }
149
+ ]
150
+ }
151
+ }
152
+ }
153
+ });
154
+
155
+ const tempRoot = dirname(dirname(configPath));
156
+ const env = {
157
+ COZE_BASE_URL: "https://coze.example.com/v1",
158
+ COZE_API_KEY: "coze-secret",
159
+ WORKSPACE_ID: "workspace-1",
160
+ TRACE_ID: "trace-1"
161
+ };
162
+
163
+ const resolved = resolveRuntimeModel({
164
+ configPath,
165
+ env
166
+ });
167
+
168
+ assert.ok(resolved);
169
+ assert.equal(resolved.model.provider, "coze");
170
+ assert.equal(resolved.model.api, "openai-completions");
171
+ assert.equal(resolved.model.baseUrl, env.COZE_BASE_URL);
172
+ assert.equal(resolved.apiKey, env.COZE_API_KEY);
173
+ assert.deepEqual(resolved.model.headers, {
174
+ "X-Workspace": env.WORKSPACE_ID,
175
+ "X-Trace": env.TRACE_ID
176
+ });
177
+
178
+ const authStorage = AuthStorage.inMemory();
179
+ const modelRegistry = new ModelRegistry(authStorage, join(tempRoot, "absent-models.json"));
180
+
181
+ authStorage.setRuntimeApiKey(resolved.model.provider, resolved.apiKey!);
182
+
183
+ const auth = await modelRegistry.getApiKeyAndHeaders(resolved.model);
184
+ rmSync(tempRoot, { recursive: true, force: true });
185
+
186
+ assert.deepEqual(auth, {
187
+ ok: true,
188
+ apiKey: env.COZE_API_KEY,
189
+ headers: {
190
+ "X-Workspace": env.WORKSPACE_ID,
191
+ "X-Trace": env.TRACE_ID
192
+ }
193
+ });
194
+ });
195
+
196
+ test("resolveRuntimeModel applies workspace overrides to built-in providers", () => {
197
+ const configPath = createConfigFile({
198
+ agents: {
199
+ defaults: {
200
+ workspace: ".",
201
+ model: {
202
+ primary: "openai/gpt-5-mini"
203
+ }
204
+ }
205
+ },
206
+ models: {
207
+ providers: {
208
+ openai: {
209
+ baseUrl: "https://proxy.example.com/v1",
210
+ headers: {
211
+ "X-Proxy": "${PROXY_ID}"
212
+ },
213
+ modelOverrides: {
214
+ "gpt-5-mini": {
215
+ maxTokens: 4096
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ });
222
+
223
+ const tempRoot = dirname(dirname(configPath));
224
+ const resolved = resolveRuntimeModel({
225
+ provider: "openai",
226
+ model: "gpt-5-mini",
227
+ baseUrl: "https://forced.example.com/v1",
228
+ configPath,
229
+ env: {
230
+ PROXY_ID: "proxy-1"
231
+ }
232
+ });
233
+
234
+ rmSync(tempRoot, { recursive: true, force: true });
235
+
236
+ assert.ok(resolved);
237
+ assert.equal(resolved.model.provider, "openai");
238
+ assert.equal(resolved.model.id, "gpt-5-mini");
239
+ assert.equal(resolved.model.baseUrl, "https://forced.example.com/v1");
240
+ assert.equal(resolved.model.maxTokens, 4096);
241
+ assert.equal(resolved.model.headers?.["X-Proxy"], "proxy-1");
242
+ });
243
+
244
+ test("loadBotAppConfig builds the app config from config.json plus env-backed channels", () => {
245
+ const configPath = createConfigFile({
246
+ agents: {
247
+ defaults: {
248
+ workspace: ".",
249
+ thinkingLevel: "high",
250
+ model: {
251
+ primary: "coze/auto"
252
+ }
253
+ }
254
+ },
255
+ models: {
256
+ providers: {
257
+ coze: {
258
+ api: "openai-completions",
259
+ apiKey: "${COZE_API_KEY}",
260
+ baseUrl: "${COZE_BASE_URL}",
261
+ models: [
262
+ {
263
+ id: "auto",
264
+ name: "Auto",
265
+ input: ["text"],
266
+ reasoning: false
267
+ }
268
+ ]
269
+ }
270
+ }
271
+ },
272
+ channels: {
273
+ feishu: {
274
+ enabled: true,
275
+ appId: "${FEISHU_APP_ID}",
276
+ appSecret: "${FEISHU_APP_SECRET}",
277
+ domain: "${FEISHU_DOMAIN}",
278
+ encryptKey: "${FEISHU_ENCRYPT_KEY}",
279
+ verificationToken: "${FEISHU_VERIFICATION_TOKEN}",
280
+ requireMention: false,
281
+ thinkingReaction: {
282
+ enabled: false,
283
+ emojiType: "OneSecond"
284
+ }
285
+ }
286
+ }
287
+ });
288
+ const tempRoot = dirname(dirname(configPath));
289
+ const loaded = loadBotAppConfig({
290
+ configPath,
291
+ env: {
292
+ PI_BOT_AGENT_MODE: "pi",
293
+ FEISHU_APP_ID: "app-id",
294
+ FEISHU_APP_SECRET: "app-secret",
295
+ FEISHU_DOMAIN: "feishu.example.com",
296
+ FEISHU_ENCRYPT_KEY: "encrypt-key",
297
+ FEISHU_VERIFICATION_TOKEN: "verify-token"
298
+ }
299
+ });
300
+
301
+ rmSync(tempRoot, { recursive: true, force: true });
302
+
303
+ assert.equal(getDefaultConfigPath(), resolve(process.cwd(), "../workspace/config.json"));
304
+ assert.equal(loaded.agent.mode, "pi");
305
+ assert.equal(loaded.agent.provider, "coze");
306
+ assert.equal(loaded.agent.model, "auto");
307
+ assert.equal(loaded.agent.configPath, configPath);
308
+ assert.equal(loaded.agent.thinkingLevel, "high");
309
+ assert.equal(loaded.agent.cwd, dirname(configPath));
310
+ assert.equal(loaded.channels.feishu?.appId, "app-id");
311
+ assert.equal(loaded.channels.feishu?.verificationToken, "verify-token");
312
+ assert.equal(loaded.routing.feishuGroupRequireMention, false);
313
+ assert.equal(loaded.channels.feishu?.thinkingReaction?.enabled, false);
314
+ assert.equal(loaded.channels.feishu?.thinkingReaction?.emojiType, "OneSecond");
315
+ });
@@ -0,0 +1,125 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import test from "node:test";
6
+ import { readDocsResponse } from "../src/dashboard/api/docs.js";
7
+
8
+ function writeDoc(rootDir: string, relativePath: string, content: string) {
9
+ const filePath = join(rootDir, relativePath);
10
+ mkdirSync(dirname(filePath), { recursive: true });
11
+ writeFileSync(filePath, content, "utf-8");
12
+ }
13
+
14
+ test("readDocsResponse lists markdown docs by order and picks the first doc by default", () => {
15
+ const docsDir = mkdtempSync(join(tmpdir(), "pi-bot-dashboard-docs-"));
16
+
17
+ try {
18
+ writeDoc(
19
+ docsDir,
20
+ "channels/feishu.md",
21
+ `---
22
+ title: 飞书接入
23
+ summary: 飞书文档
24
+ group: 渠道
25
+ order: 30
26
+ ---
27
+ # 飞书接入
28
+
29
+ 这里是飞书说明。
30
+ `
31
+ );
32
+ writeDoc(
33
+ docsDir,
34
+ "getting-started.md",
35
+ `---
36
+ title: 快速开始
37
+ summary: 启动项目
38
+ group: 入门
39
+ order: 10
40
+ ---
41
+ # 快速开始
42
+
43
+ 先启动项目。
44
+ `
45
+ );
46
+
47
+ const response = readDocsResponse({ docsDir });
48
+
49
+ assert.deepEqual(
50
+ response.docs.map((doc) => doc.slug),
51
+ ["getting-started", "channels/feishu"]
52
+ );
53
+ assert.equal(response.selectedDoc?.slug, "getting-started");
54
+ assert.equal(response.requestedSlugFound, true);
55
+ } finally {
56
+ rmSync(docsDir, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ test("readDocsResponse returns the requested doc and strips frontmatter from content", () => {
61
+ const docsDir = mkdtempSync(join(tmpdir(), "pi-bot-dashboard-docs-selected-"));
62
+
63
+ try {
64
+ writeDoc(
65
+ docsDir,
66
+ "getting-started.md",
67
+ `---
68
+ title: 快速开始
69
+ summary: 启动项目
70
+ group: 入门
71
+ order: 10
72
+ ---
73
+ # 快速开始
74
+
75
+ 先启动项目。
76
+ `
77
+ );
78
+ writeDoc(
79
+ docsDir,
80
+ "channels/feishu.md",
81
+ `---
82
+ title: 飞书接入
83
+ summary: 飞书文档
84
+ group: 渠道
85
+ order: 30
86
+ ---
87
+ # 飞书接入
88
+
89
+ 这里是飞书说明。
90
+ `
91
+ );
92
+
93
+ const response = readDocsResponse({ docsDir, slug: "channels/feishu" });
94
+
95
+ assert.equal(response.selectedDoc?.slug, "channels/feishu");
96
+ assert.match(response.selectedDoc?.content ?? "", /^# 飞书接入/m);
97
+ assert.doesNotMatch(response.selectedDoc?.content ?? "", /^---$/m);
98
+ assert.equal(response.requestedSlugFound, true);
99
+ } finally {
100
+ rmSync(docsDir, { recursive: true, force: true });
101
+ }
102
+ });
103
+
104
+ test("readDocsResponse falls back to the default doc when the requested slug is missing", () => {
105
+ const docsDir = mkdtempSync(join(tmpdir(), "pi-bot-dashboard-docs-fallback-"));
106
+
107
+ try {
108
+ writeDoc(
109
+ docsDir,
110
+ "getting-started.md",
111
+ `# 快速开始
112
+
113
+ 先启动项目。
114
+ `
115
+ );
116
+
117
+ const response = readDocsResponse({ docsDir, slug: "does-not-exist" });
118
+
119
+ assert.equal(response.selectedDoc?.slug, "getting-started");
120
+ assert.equal(response.requestedSlug, "does-not-exist");
121
+ assert.equal(response.requestedSlugFound, false);
122
+ } finally {
123
+ rmSync(docsDir, { recursive: true, force: true });
124
+ }
125
+ });
@@ -0,0 +1,171 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { ValidationError, readModelsResponse, saveModelsRequest } from "../src/dashboard/api/models.js";
7
+
8
+ function createConfigFile(config: unknown): { configPath: string; cleanup: () => void } {
9
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-bot-dashboard-models-"));
10
+ const workspaceDir = join(tempDir, "workspace");
11
+ const configPath = join(workspaceDir, "config.json");
12
+
13
+ mkdirSync(workspaceDir, { recursive: true });
14
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
15
+
16
+ return {
17
+ configPath,
18
+ cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
19
+ };
20
+ }
21
+
22
+ test("readModelsResponse exposes default model and selectable options", () => {
23
+ const { configPath, cleanup } = createConfigFile({
24
+ agents: {
25
+ defaults: {
26
+ model: {
27
+ primary: "coze/auto",
28
+ },
29
+ },
30
+ },
31
+ models: {
32
+ providers: {
33
+ coze: {
34
+ api: "openai-completions",
35
+ apiKey: "${COZE_API_KEY}",
36
+ baseUrl: "https://coze.example.com/v1",
37
+ authHeader: true,
38
+ models: [
39
+ {
40
+ id: "auto",
41
+ name: "Auto",
42
+ input: ["text", "image"],
43
+ reasoning: true,
44
+ contextWindow: 200000,
45
+ maxTokens: 8192,
46
+ },
47
+ ],
48
+ },
49
+ },
50
+ },
51
+ });
52
+
53
+ const result = readModelsResponse({ configPath });
54
+ cleanup();
55
+
56
+ assert.equal(result.defaultModel, "coze/auto");
57
+ assert.deepEqual(result.options, [{ value: "coze/auto", label: "coze / Auto" }]);
58
+ });
59
+
60
+ test("saveModelsRequest updates default model while preserving unrelated config", () => {
61
+ const { configPath, cleanup } = createConfigFile({
62
+ agents: {
63
+ defaults: {
64
+ model: {
65
+ primary: "coze/auto",
66
+ },
67
+ },
68
+ },
69
+ models: {
70
+ providers: {
71
+ coze: {
72
+ api: "openai-completions",
73
+ apiKey: "${COZE_API_KEY}",
74
+ baseUrl: "https://coze.example.com/v1",
75
+ headers: {
76
+ "X-Provider": "keep-me",
77
+ },
78
+ models: [
79
+ {
80
+ id: "auto",
81
+ name: "Auto",
82
+ input: ["text"],
83
+ reasoning: false,
84
+ contextWindow: 200000,
85
+ maxTokens: 8192,
86
+ cost: {
87
+ input: 0,
88
+ output: 0,
89
+ cacheRead: 0,
90
+ cacheWrite: 0,
91
+ },
92
+ },
93
+ {
94
+ id: "glm-4.7",
95
+ name: "GLM-4.7",
96
+ input: ["text"],
97
+ reasoning: false,
98
+ contextWindow: 256000,
99
+ maxTokens: 8192,
100
+ cost: {
101
+ input: 0,
102
+ output: 0,
103
+ cacheRead: 0,
104
+ cacheWrite: 0,
105
+ },
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ },
111
+ channels: {
112
+ feishu: {
113
+ enabled: true,
114
+ },
115
+ },
116
+ });
117
+
118
+ saveModelsRequest({
119
+ configPath,
120
+ body: {
121
+ models: {
122
+ defaultModel: "coze/glm-4.7",
123
+ },
124
+ },
125
+ });
126
+
127
+ const saved = JSON.parse(readFileSync(configPath, "utf-8")) as {
128
+ agents: { defaults: { model: { primary: string } } };
129
+ models: {
130
+ providers: {
131
+ coze: {
132
+ apiKey: string;
133
+ baseUrl: string;
134
+ authHeader: boolean;
135
+ headers: Record<string, string>;
136
+ models: Array<Record<string, unknown>>;
137
+ };
138
+ };
139
+ };
140
+ channels: { feishu: { enabled: boolean } };
141
+ };
142
+
143
+ cleanup();
144
+
145
+ assert.equal(saved.agents.defaults.model.primary, "coze/glm-4.7");
146
+ assert.deepEqual(saved.models.providers.coze.headers, {
147
+ "X-Provider": "keep-me",
148
+ });
149
+ assert.equal(saved.channels.feishu.enabled, true);
150
+ });
151
+
152
+ test("saveModelsRequest rejects invalid defaultModel", () => {
153
+ const { configPath, cleanup } = createConfigFile({
154
+ agents: { defaults: { model: { primary: "coze/auto" } } },
155
+ models: { providers: { coze: { models: [{ id: "auto", name: "Auto" }] } } },
156
+ });
157
+ try {
158
+ assert.throws(() => {
159
+ saveModelsRequest({
160
+ configPath,
161
+ body: {
162
+ models: {
163
+ defaultModel: "coze/does-not-exist",
164
+ },
165
+ },
166
+ });
167
+ }, ValidationError);
168
+ } finally {
169
+ cleanup();
170
+ }
171
+ });