@coze-arch/cli 0.0.13 → 0.0.14-alpha.c52ee4

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 (76) hide show
  1. package/lib/__templates__/expo/AGENTS.md +15 -7
  2. package/lib/__templates__/expo/README.md +15 -7
  3. package/lib/__templates__/expo/client/eslint.config.mjs +3 -0
  4. package/lib/__templates__/expo/eslint-plugins/expo/index.js +9 -0
  5. package/lib/__templates__/expo/eslint-plugins/expo/rule.js +105 -0
  6. package/lib/__templates__/expo/eslint-plugins/expo/tech.md +108 -0
  7. package/lib/__templates__/nextjs/AGENTS.md +9 -0
  8. package/lib/__templates__/nextjs/eslint.config.mjs +15 -0
  9. package/lib/__templates__/pi-agent/.coze +10 -0
  10. package/lib/__templates__/pi-agent/AGENTS.md +150 -0
  11. package/lib/__templates__/pi-agent/README.md +155 -0
  12. package/lib/__templates__/pi-agent/_gitignore +3 -0
  13. package/lib/__templates__/pi-agent/docs/project-overview.md +273 -0
  14. package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
  15. package/lib/__templates__/pi-agent/package.json +52 -0
  16. package/lib/__templates__/pi-agent/pnpm-lock.yaml +7840 -0
  17. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  18. package/lib/__templates__/pi-agent/scripts/prepare.sh +2 -0
  19. package/lib/__templates__/pi-agent/src/agent.ts +367 -0
  20. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  21. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  22. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  23. package/lib/__templates__/pi-agent/src/config.ts +596 -0
  24. package/lib/__templates__/pi-agent/src/core.ts +218 -0
  25. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +148 -0
  26. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
  27. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +141 -0
  28. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  29. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  30. package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
  31. package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
  32. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  33. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  34. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  35. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
  36. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  37. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  38. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  58. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  59. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  60. package/lib/__templates__/pi-agent/template.config.js +45 -0
  61. package/lib/__templates__/pi-agent/tests/config.test.ts +292 -0
  62. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
  63. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  64. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  65. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  66. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  67. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  68. package/lib/__templates__/pi-agent/tsconfig.json +20 -0
  69. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  70. package/lib/__templates__/taro/pnpm-lock.yaml +24 -14
  71. package/lib/__templates__/taro/server/package.json +0 -2
  72. package/lib/__templates__/taro/src/presets/dev-debug.ts +2 -2
  73. package/lib/__templates__/templates.json +24 -0
  74. package/lib/__templates__/vite/AGENTS.md +5 -0
  75. package/lib/cli.js +1 -1
  76. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+ export const paramsSchema = {
11
+ type: 'object',
12
+ properties: {
13
+ port: {
14
+ type: 'number',
15
+ default: 5000,
16
+ minimum: 1024,
17
+ maximum: 65535,
18
+ description: 'Dashboard server port',
19
+ },
20
+ workspaceDir: {
21
+ type: 'string',
22
+ default: '/workspace/workspace',
23
+ description: 'Workspace directory path',
24
+ },
25
+ },
26
+ required: [],
27
+ additionalProperties: false,
28
+ };
29
+
30
+ const config = {
31
+ description:
32
+ 'Pi Agent:`coze init ${COZE_WORKSPACE_PATH} --template pi-agent`\n' +
33
+ '- 适用:基于 pi-agent-core 的 AI Agent 应用\n' +
34
+ '- 支持飞书、微信等多渠道接入\n' +
35
+ '- 内置 Dashboard 管理面板\n' +
36
+ '- 使用 TypeScript + Express + Vite',
37
+ paramsSchema,
38
+
39
+ defaultParams: {
40
+ port: 5000,
41
+ workspaceDir: '/workspace/workspace',
42
+ },
43
+ };
44
+
45
+ export default config;
@@ -0,0 +1,292 @@
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("resolveRuntimeModel builds a custom provider model from workspace/config.json", async () => {
77
+ const configPath = createConfigFile({
78
+ agents: {
79
+ defaults: {
80
+ workspace: ".",
81
+ model: {
82
+ primary: "coze/glm-4-7-251222"
83
+ }
84
+ }
85
+ },
86
+ models: {
87
+ providers: {
88
+ missing: {
89
+ api: "openai-completions",
90
+ baseUrl: "${MISSING_BASE_URL}",
91
+ apiKey: "${MISSING_API_KEY}",
92
+ models: [
93
+ {
94
+ id: "unused-model",
95
+ name: "Unused",
96
+ input: ["text"],
97
+ reasoning: false
98
+ }
99
+ ]
100
+ },
101
+ coze: {
102
+ api: "openai-completions",
103
+ baseUrl: "${COZE_BASE_URL}",
104
+ apiKey: "${COZE_API_KEY}",
105
+ headers: {
106
+ "X-Workspace": "${WORKSPACE_ID}"
107
+ },
108
+ models: [
109
+ {
110
+ id: "glm-4-7-251222",
111
+ name: "GLM 4.7",
112
+ input: ["text"],
113
+ reasoning: false,
114
+ contextWindow: 200000,
115
+ maxTokens: 8192,
116
+ headers: {
117
+ "X-Trace": "${TRACE_ID}"
118
+ },
119
+ cost: {
120
+ input: 0,
121
+ output: 0,
122
+ cacheRead: 0,
123
+ cacheWrite: 0
124
+ }
125
+ }
126
+ ]
127
+ }
128
+ }
129
+ }
130
+ });
131
+
132
+ const tempRoot = dirname(dirname(configPath));
133
+ const env = {
134
+ COZE_BASE_URL: "https://coze.example.com/v1",
135
+ COZE_API_KEY: "coze-secret",
136
+ WORKSPACE_ID: "workspace-1",
137
+ TRACE_ID: "trace-1"
138
+ };
139
+
140
+ const resolved = resolveRuntimeModel({
141
+ configPath,
142
+ env
143
+ });
144
+
145
+ assert.ok(resolved);
146
+ assert.equal(resolved.model.provider, "coze");
147
+ assert.equal(resolved.model.api, "openai-completions");
148
+ assert.equal(resolved.model.baseUrl, env.COZE_BASE_URL);
149
+ assert.equal(resolved.apiKey, env.COZE_API_KEY);
150
+ assert.deepEqual(resolved.model.headers, {
151
+ "X-Workspace": env.WORKSPACE_ID,
152
+ "X-Trace": env.TRACE_ID
153
+ });
154
+
155
+ const authStorage = AuthStorage.inMemory();
156
+ const modelRegistry = new ModelRegistry(authStorage, join(tempRoot, "absent-models.json"));
157
+
158
+ authStorage.setRuntimeApiKey(resolved.model.provider, resolved.apiKey!);
159
+
160
+ const auth = await modelRegistry.getApiKeyAndHeaders(resolved.model);
161
+ rmSync(tempRoot, { recursive: true, force: true });
162
+
163
+ assert.deepEqual(auth, {
164
+ ok: true,
165
+ apiKey: env.COZE_API_KEY,
166
+ headers: {
167
+ "X-Workspace": env.WORKSPACE_ID,
168
+ "X-Trace": env.TRACE_ID
169
+ }
170
+ });
171
+ });
172
+
173
+ test("resolveRuntimeModel applies workspace overrides to built-in providers", () => {
174
+ const configPath = createConfigFile({
175
+ agents: {
176
+ defaults: {
177
+ workspace: ".",
178
+ model: {
179
+ primary: "openai/gpt-5-mini"
180
+ }
181
+ }
182
+ },
183
+ models: {
184
+ providers: {
185
+ openai: {
186
+ baseUrl: "https://proxy.example.com/v1",
187
+ headers: {
188
+ "X-Proxy": "${PROXY_ID}"
189
+ },
190
+ modelOverrides: {
191
+ "gpt-5-mini": {
192
+ maxTokens: 4096
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ });
199
+
200
+ const tempRoot = dirname(dirname(configPath));
201
+ const resolved = resolveRuntimeModel({
202
+ provider: "openai",
203
+ model: "gpt-5-mini",
204
+ baseUrl: "https://forced.example.com/v1",
205
+ configPath,
206
+ env: {
207
+ PROXY_ID: "proxy-1"
208
+ }
209
+ });
210
+
211
+ rmSync(tempRoot, { recursive: true, force: true });
212
+
213
+ assert.ok(resolved);
214
+ assert.equal(resolved.model.provider, "openai");
215
+ assert.equal(resolved.model.id, "gpt-5-mini");
216
+ assert.equal(resolved.model.baseUrl, "https://forced.example.com/v1");
217
+ assert.equal(resolved.model.maxTokens, 4096);
218
+ assert.equal(resolved.model.headers?.["X-Proxy"], "proxy-1");
219
+ });
220
+
221
+ test("loadBotAppConfig builds the app config from config.json plus env-backed channels", () => {
222
+ const configPath = createConfigFile({
223
+ agents: {
224
+ defaults: {
225
+ workspace: ".",
226
+ thinkingLevel: "high",
227
+ model: {
228
+ primary: "coze/auto"
229
+ }
230
+ }
231
+ },
232
+ models: {
233
+ providers: {
234
+ coze: {
235
+ api: "openai-completions",
236
+ apiKey: "${COZE_API_KEY}",
237
+ baseUrl: "${COZE_BASE_URL}",
238
+ models: [
239
+ {
240
+ id: "auto",
241
+ name: "Auto",
242
+ input: ["text"],
243
+ reasoning: false
244
+ }
245
+ ]
246
+ }
247
+ }
248
+ },
249
+ channels: {
250
+ feishu: {
251
+ enabled: true,
252
+ appId: "${FEISHU_APP_ID}",
253
+ appSecret: "${FEISHU_APP_SECRET}",
254
+ domain: "${FEISHU_DOMAIN}",
255
+ encryptKey: "${FEISHU_ENCRYPT_KEY}",
256
+ verificationToken: "${FEISHU_VERIFICATION_TOKEN}",
257
+ requireMention: false,
258
+ thinkingReaction: {
259
+ enabled: false,
260
+ emojiType: "OneSecond"
261
+ }
262
+ }
263
+ }
264
+ });
265
+ const tempRoot = dirname(dirname(configPath));
266
+ const loaded = loadBotAppConfig({
267
+ configPath,
268
+ env: {
269
+ PI_BOT_AGENT_MODE: "pi",
270
+ FEISHU_APP_ID: "app-id",
271
+ FEISHU_APP_SECRET: "app-secret",
272
+ FEISHU_DOMAIN: "feishu.example.com",
273
+ FEISHU_ENCRYPT_KEY: "encrypt-key",
274
+ FEISHU_VERIFICATION_TOKEN: "verify-token"
275
+ }
276
+ });
277
+
278
+ rmSync(tempRoot, { recursive: true, force: true });
279
+
280
+ assert.equal(getDefaultConfigPath(), resolve(process.cwd(), "../workspace/config.json"));
281
+ assert.equal(loaded.agent.mode, "pi");
282
+ assert.equal(loaded.agent.provider, "coze");
283
+ assert.equal(loaded.agent.model, "auto");
284
+ assert.equal(loaded.agent.configPath, configPath);
285
+ assert.equal(loaded.agent.thinkingLevel, "high");
286
+ assert.equal(loaded.agent.cwd, dirname(configPath));
287
+ assert.equal(loaded.channels.feishu?.appId, "app-id");
288
+ assert.equal(loaded.channels.feishu?.verificationToken, "verify-token");
289
+ assert.equal(loaded.routing.feishuGroupRequireMention, false);
290
+ assert.equal(loaded.channels.feishu?.thinkingReaction?.enabled, false);
291
+ assert.equal(loaded.channels.feishu?.thinkingReaction?.emojiType, "OneSecond");
292
+ });
@@ -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
+ });