@coze-arch/cli 0.0.17 → 0.0.19-beta.1

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 (104) hide show
  1. package/lib/__templates__/expo/.coze +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
  3. package/lib/__templates__/expo/package.json +2 -1
  4. package/lib/__templates__/nextjs/.coze +1 -0
  5. package/lib/__templates__/nextjs/package.json +3 -1
  6. package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
  7. package/lib/__templates__/nuxt-vue/.coze +1 -0
  8. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  9. package/lib/__templates__/nuxt-vue/package.json +9 -2
  10. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  11. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  12. package/lib/__templates__/pi-agent/.coze +10 -0
  13. package/lib/__templates__/pi-agent/AGENTS.md +144 -0
  14. package/lib/__templates__/pi-agent/README.md +216 -0
  15. package/lib/__templates__/pi-agent/_gitignore +3 -0
  16. package/lib/__templates__/pi-agent/_npmrc +23 -0
  17. package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
  18. package/lib/__templates__/pi-agent/docs/project-overview.md +374 -0
  19. package/lib/__templates__/pi-agent/docs/user/getting-started.md +47 -0
  20. package/lib/__templates__/pi-agent/package.json +63 -0
  21. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  22. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  23. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +36 -0
  24. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +9 -0
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +41 -0
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +9 -0
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +85 -0
  28. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +9 -0
  29. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +53 -0
  30. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +9 -0
  31. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
  32. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  33. package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
  34. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  35. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  36. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  37. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  38. package/lib/__templates__/pi-agent/src/cli.ts +117 -0
  39. package/lib/__templates__/pi-agent/src/config.ts +708 -0
  40. package/lib/__templates__/pi-agent/src/core.ts +218 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
  66. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
  67. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
  68. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
  69. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  70. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
  71. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  72. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  73. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  74. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  75. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  76. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  77. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  78. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  79. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  80. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  81. package/lib/__templates__/pi-agent/template.config.js +45 -0
  82. package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
  83. package/lib/__templates__/pi-agent/tests/config.test.ts +315 -0
  84. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
  85. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  86. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  87. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  88. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  89. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  90. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  91. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  92. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  93. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  94. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  95. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  96. package/lib/__templates__/taro/.coze +1 -0
  97. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  98. package/lib/__templates__/taro/package.json +1 -1
  99. package/lib/__templates__/templates.json +18 -31
  100. package/lib/__templates__/vite/.coze +1 -0
  101. package/lib/__templates__/vite/package.json +3 -1
  102. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  103. package/lib/cli.js +13 -2
  104. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createMemoryConfigStore } from "../src/dashboard/config-store.js";
4
+ import { runConfigCommand, type CliContext } from "../src/cli.js";
5
+
6
+ function createTestContext(initial: Record<string, unknown> = {}) {
7
+ const store = createMemoryConfigStore(initial);
8
+ const logs: string[] = [];
9
+ const errors: string[] = [];
10
+ const ctx: CliContext = {
11
+ configStore: store,
12
+ log: (msg) => logs.push(msg),
13
+ error: (msg) => errors.push(msg),
14
+ };
15
+ return { store, logs, errors, ctx };
16
+ }
17
+
18
+ test("runConfigCommand returns false for non-config commands", () => {
19
+ const { ctx } = createTestContext();
20
+ assert.equal(runConfigCommand([], ctx), false);
21
+ assert.equal(runConfigCommand(["start"], ctx), false);
22
+ assert.equal(runConfigCommand(["run"], ctx), false);
23
+ });
24
+
25
+ test("config set writes a string value at a nested path", () => {
26
+ const { store, logs, ctx } = createTestContext({ channels: { feishu: { appId: "old" } } });
27
+ runConfigCommand(["config", "set", "channels.feishu.appId", "cli_new"], ctx);
28
+ const snapshot = store.snapshot();
29
+ assert.equal((snapshot.channels as Record<string, Record<string, unknown>>).feishu.appId, "cli_new");
30
+ assert.ok(logs[0]?.includes("cli_new"));
31
+ });
32
+
33
+ test("config set coerces boolean values", () => {
34
+ const { store, ctx } = createTestContext({ channels: { feishu: { enabled: false } } });
35
+ runConfigCommand(["config", "set", "channels.feishu.enabled", "true"], ctx);
36
+ const snapshot = store.snapshot();
37
+ assert.equal((snapshot.channels as Record<string, Record<string, unknown>>).feishu.enabled, true);
38
+ });
39
+
40
+ test("config set coerces numeric values", () => {
41
+ const { store, ctx } = createTestContext({ models: { maxTokens: 8192 } });
42
+ runConfigCommand(["config", "set", "models.maxTokens", "16384"], ctx);
43
+ const snapshot = store.snapshot();
44
+ assert.equal((snapshot.models as Record<string, unknown>).maxTokens, 16384);
45
+ });
46
+
47
+ test("config set creates intermediate objects for missing paths", () => {
48
+ const { store, ctx } = createTestContext({});
49
+ runConfigCommand(["config", "set", "channels.feishu.appId", "new-id"], ctx);
50
+ const snapshot = store.snapshot();
51
+ assert.equal(
52
+ (snapshot.channels as Record<string, Record<string, unknown>>).feishu.appId,
53
+ "new-id",
54
+ );
55
+ });
56
+
57
+ test("config set handles array index paths", () => {
58
+ const { store, ctx } = createTestContext({ items: [{ name: "a" }, { name: "b" }] });
59
+ runConfigCommand(["config", "set", "items.1.name", "updated"], ctx);
60
+ const snapshot = store.snapshot();
61
+ assert.equal((snapshot.items as Array<Record<string, unknown>>)[1]!.name, "updated");
62
+ });
63
+
64
+ test("config get returns a string value", () => {
65
+ const { logs, ctx } = createTestContext({ channels: { feishu: { appId: "test-id" } } });
66
+ runConfigCommand(["config", "get", "channels.feishu.appId"], ctx);
67
+ assert.equal(logs[0], "test-id");
68
+ });
69
+
70
+ test("config get returns a formatted object", () => {
71
+ const { logs, ctx } = createTestContext({ channels: { feishu: { appId: "x", enabled: true } } });
72
+ runConfigCommand(["config", "get", "channels.feishu"], ctx);
73
+ const parsed = JSON.parse(logs[0]!) as Record<string, unknown>;
74
+ assert.equal(parsed.appId, "x");
75
+ assert.equal(parsed.enabled, true);
76
+ });
77
+
78
+ test("config get reports error for missing key", () => {
79
+ const { errors, ctx } = createTestContext({});
80
+ runConfigCommand(["config", "get", "missing.key"], ctx);
81
+ assert.ok(errors[0]?.includes("not found"));
82
+ });
83
+
84
+ test("config list outputs the full config", () => {
85
+ const original = { channels: { feishu: { enabled: true } } };
86
+ const { logs, ctx } = createTestContext(original);
87
+ runConfigCommand(["config", "list"], ctx);
88
+ const parsed = JSON.parse(logs[0]!) as Record<string, unknown>;
89
+ assert.deepEqual(parsed, original);
90
+ });
91
+
92
+ test("config delete removes a key", () => {
93
+ const { store, logs, ctx } = createTestContext({
94
+ channels: { feishu: { appId: "x", enabled: true } },
95
+ });
96
+ runConfigCommand(["config", "delete", "channels.feishu.appId"], ctx);
97
+ const snapshot = store.snapshot();
98
+ const feishu = (snapshot.channels as Record<string, Record<string, unknown>>).feishu;
99
+ assert.equal("appId" in feishu, false);
100
+ assert.equal(feishu.enabled, true);
101
+ assert.ok(logs[0]?.includes("Deleted"));
102
+ });
103
+
104
+ test("config delete reports error for missing key", () => {
105
+ const { errors, ctx } = createTestContext({});
106
+ runConfigCommand(["config", "delete", "missing.key"], ctx);
107
+ assert.ok(errors[0]?.includes("not found"));
108
+ });
109
+
110
+ test("config with no subcommand prints usage", () => {
111
+ const { logs, ctx } = createTestContext();
112
+ const handled = runConfigCommand(["config"], ctx);
113
+ assert.equal(handled, true);
114
+ assert.ok(logs.some((l) => l.includes("Usage")));
115
+ });
116
+
117
+ test("config with unknown subcommand prints error and usage", () => {
118
+ const { errors, logs, ctx } = createTestContext();
119
+ const handled = runConfigCommand(["config", "badcmd"], ctx);
120
+ assert.equal(handled, true);
121
+ assert.ok(errors[0]?.includes("Unknown"));
122
+ assert.ok(logs.some((l) => l.includes("Usage")));
123
+ });
124
+
125
+ test("config set with missing arguments prints usage", () => {
126
+ const { errors, ctx } = createTestContext();
127
+ runConfigCommand(["config", "set", "key"], ctx);
128
+ assert.ok(errors[0]?.includes("Usage"));
129
+ });
130
+
131
+ test("config set coerces null value", () => {
132
+ const { store, ctx } = createTestContext({ key: "value" });
133
+ runConfigCommand(["config", "set", "key", "null"], ctx);
134
+ const snapshot = store.snapshot();
135
+ assert.equal(snapshot.key, null);
136
+ });
@@ -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
+ });