@coze-arch/cli 0.0.18 → 0.0.19-alpha.502ddf

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 (97) 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/app/pages/index.vue +6 -0
  9. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  10. package/lib/__templates__/nuxt-vue/nuxt.config.ts +2 -2
  11. package/lib/__templates__/nuxt-vue/package.json +9 -2
  12. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  13. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  14. package/lib/__templates__/pi-agent/.coze +10 -0
  15. package/lib/__templates__/pi-agent/AGENTS.md +149 -0
  16. package/lib/__templates__/pi-agent/README.md +218 -0
  17. package/lib/__templates__/pi-agent/_gitignore +3 -0
  18. package/lib/__templates__/pi-agent/_npmrc +23 -0
  19. package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
  20. package/lib/__templates__/pi-agent/docs/project-overview.md +368 -0
  21. package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
  22. package/lib/__templates__/pi-agent/package.json +63 -0
  23. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  24. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +30 -0
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +29 -0
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +57 -0
  28. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +40 -0
  29. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
  30. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  31. package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
  32. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  33. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  34. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  35. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  36. package/lib/__templates__/pi-agent/src/cli.ts +117 -0
  37. package/lib/__templates__/pi-agent/src/config.ts +749 -0
  38. package/lib/__templates__/pi-agent/src/core.ts +219 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/index.ts +74 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/server.ts +610 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +172 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +24 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +440 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +330 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +203 -0
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  66. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  67. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  68. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  69. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  70. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  71. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  72. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  73. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  74. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  75. package/lib/__templates__/pi-agent/template.config.js +45 -0
  76. package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
  77. package/lib/__templates__/pi-agent/tests/config.test.ts +377 -0
  78. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  79. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  80. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  81. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  82. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  83. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  84. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  85. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  86. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  87. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  88. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  89. package/lib/__templates__/taro/.coze +1 -0
  90. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  91. package/lib/__templates__/taro/package.json +1 -1
  92. package/lib/__templates__/templates.json +24 -0
  93. package/lib/__templates__/vite/.coze +1 -0
  94. package/lib/__templates__/vite/package.json +3 -1
  95. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  96. package/lib/cli.js +13 -2
  97. package/package.json +1 -1
@@ -0,0 +1,297 @@
1
+ import type { Client } from "@larksuiteoapi/node-sdk";
2
+
3
+ type FeishuDomain = string | undefined;
4
+
5
+ type Credentials = {
6
+ appId: string;
7
+ appSecret: string;
8
+ domain?: FeishuDomain;
9
+ };
10
+
11
+ type CardState = {
12
+ cardId: string;
13
+ messageId: string;
14
+ sequence: number;
15
+ currentText: string;
16
+ };
17
+
18
+ type StreamingStartOptions = {
19
+ replyToMessageId: string;
20
+ replyInThread?: boolean;
21
+ };
22
+
23
+ const tokenCache = new Map<string, { token: string; expiresAt: number }>();
24
+
25
+ function resolveApiBase(domain?: FeishuDomain): string {
26
+ if (domain === "lark") {
27
+ return "https://open.larksuite.com/open-apis";
28
+ }
29
+ if (domain && domain !== "feishu" && domain.startsWith("http")) {
30
+ return `${domain.replace(/\/+$/, "")}/open-apis`;
31
+ }
32
+ return "https://open.feishu.cn/open-apis";
33
+ }
34
+
35
+ async function getToken(creds: Credentials): Promise<string> {
36
+ const key = `${creds.domain ?? "feishu"}|${creds.appId}`;
37
+ const cached = tokenCache.get(key);
38
+ if (cached && cached.expiresAt > Date.now() + 60_000) {
39
+ return cached.token;
40
+ }
41
+
42
+ const response = await fetch(`${resolveApiBase(creds.domain)}/auth/v3/tenant_access_token/internal`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({ app_id: creds.appId, app_secret: creds.appSecret }),
46
+ });
47
+ if (!response.ok) {
48
+ throw new Error(`Token request failed with HTTP ${response.status}`);
49
+ }
50
+
51
+ const data = (await response.json()) as {
52
+ code: number;
53
+ msg: string;
54
+ tenant_access_token?: string;
55
+ expire?: number;
56
+ };
57
+ if (data.code !== 0 || !data.tenant_access_token) {
58
+ throw new Error(`Token error: ${data.msg}`);
59
+ }
60
+
61
+ tokenCache.set(key, {
62
+ token: data.tenant_access_token,
63
+ expiresAt: Date.now() + (data.expire ?? 7200) * 1000,
64
+ });
65
+ return data.tenant_access_token;
66
+ }
67
+
68
+ async function readResponseBodySafe(response: Response): Promise<string> {
69
+ try {
70
+ const text = await response.text();
71
+ return text.trim();
72
+ } catch {
73
+ return "";
74
+ }
75
+ }
76
+
77
+ function truncateSummary(text: string, max = 50): string {
78
+ const clean = text.replace(/\n/g, " ").trim();
79
+ return clean.length <= max ? clean : `${clean.slice(0, max - 3)}...`;
80
+ }
81
+
82
+ export function mergeStreamingText(previousText: string | undefined, nextText: string | undefined): string {
83
+ const previous = typeof previousText === "string" ? previousText : "";
84
+ const next = typeof nextText === "string" ? nextText : "";
85
+ if (!next) return previous;
86
+ if (!previous || next === previous) return next;
87
+ if (next.startsWith(previous)) return next;
88
+ if (previous.startsWith(next)) return previous;
89
+ if (next.includes(previous)) return next;
90
+ if (previous.includes(next)) return previous;
91
+
92
+ const maxOverlap = Math.min(previous.length, next.length);
93
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
94
+ if (previous.slice(-overlap) === next.slice(0, overlap)) {
95
+ return `${previous}${next.slice(overlap)}`;
96
+ }
97
+ }
98
+ return `${previous}${next}`;
99
+ }
100
+
101
+ export class FeishuStreamingCardSession {
102
+ private state: CardState | null = null;
103
+ private queue: Promise<void> = Promise.resolve();
104
+ private closed = false;
105
+ private pendingText: string | null = null;
106
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
107
+ private lastUpdateTime = 0;
108
+ private readonly updateThrottleMs = 100;
109
+
110
+ constructor(
111
+ private readonly client: Client,
112
+ private readonly creds: Credentials,
113
+ private readonly log?: (message: string, fields?: Record<string, unknown>) => void,
114
+ ) {}
115
+
116
+ async start(
117
+ _receiveId: string,
118
+ _receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
119
+ options?: StreamingStartOptions,
120
+ ): Promise<void> {
121
+ if (this.state) return;
122
+ if (!options?.replyToMessageId) {
123
+ throw new Error("replyToMessageId is required for feishu streaming card replies");
124
+ }
125
+
126
+ const cardJson = {
127
+ schema: "2.0",
128
+ config: {
129
+ streaming_mode: true,
130
+ summary: { content: "[Generating...]" },
131
+ streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default: 1 } },
132
+ },
133
+ body: {
134
+ elements: [{ tag: "markdown", content: "⏳ Thinking...", element_id: "content" }],
135
+ },
136
+ };
137
+
138
+ const token = await getToken(this.creds);
139
+ const createRes = await fetch(`${resolveApiBase(this.creds.domain)}/cardkit/v1/cards`, {
140
+ method: "POST",
141
+ headers: {
142
+ Authorization: `Bearer ${token}`,
143
+ "Content-Type": "application/json",
144
+ },
145
+ body: JSON.stringify({ type: "card_json", data: JSON.stringify(cardJson) }),
146
+ });
147
+ if (!createRes.ok) {
148
+ const body = await readResponseBodySafe(createRes);
149
+ throw new Error(
150
+ `Create card request failed with HTTP ${createRes.status}${body ? `: ${body}` : ""}`,
151
+ );
152
+ }
153
+
154
+ const createData = (await createRes.json()) as {
155
+ code: number;
156
+ msg: string;
157
+ data?: { card_id: string };
158
+ };
159
+ if (createData.code !== 0 || !createData.data?.card_id) {
160
+ throw new Error(`Create card failed (code=${createData.code}): ${createData.msg}`);
161
+ }
162
+
163
+ const cardId = createData.data.card_id;
164
+ const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } });
165
+ const sendRes = (await this.client.im.message.reply({
166
+ path: { message_id: options.replyToMessageId },
167
+ data: {
168
+ msg_type: "interactive",
169
+ content: cardContent,
170
+ ...(options.replyInThread ? { reply_in_thread: true } : {}),
171
+ },
172
+ })) as {
173
+ code?: number;
174
+ msg?: string;
175
+ data?: { message_id?: string };
176
+ };
177
+
178
+ if (sendRes.code !== 0 || !sendRes.data?.message_id) {
179
+ throw new Error(`Send card failed: ${sendRes.msg}`);
180
+ }
181
+ this.log?.("stream start", { cardId, messageId: sendRes.data.message_id });
182
+
183
+ this.state = {
184
+ cardId,
185
+ messageId: sendRes.data.message_id,
186
+ sequence: 1,
187
+ currentText: "",
188
+ };
189
+ }
190
+
191
+ private async updateCardContent(text: string): Promise<void> {
192
+ if (!this.state) return;
193
+ this.state.sequence += 1;
194
+ const token = await getToken(this.creds);
195
+ const response = await fetch(
196
+ `${resolveApiBase(this.creds.domain)}/cardkit/v1/cards/${this.state.cardId}/elements/content/content`,
197
+ {
198
+ method: "PUT",
199
+ headers: {
200
+ Authorization: `Bearer ${token}`,
201
+ "Content-Type": "application/json",
202
+ },
203
+ body: JSON.stringify({
204
+ content: text,
205
+ sequence: this.state.sequence,
206
+ uuid: `s_${this.state.cardId}_${this.state.sequence}`,
207
+ }),
208
+ },
209
+ );
210
+ if (!response.ok) {
211
+ const body = await readResponseBodySafe(response);
212
+ throw new Error(
213
+ `Update card request failed with HTTP ${response.status}${body ? `: ${body}` : ""}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ async update(text: string): Promise<void> {
219
+ if (!this.state || this.closed) return;
220
+ const mergedInput = mergeStreamingText(this.pendingText ?? this.state.currentText, text);
221
+ if (!mergedInput || mergedInput === this.state.currentText) return;
222
+
223
+ const now = Date.now();
224
+ if (now - this.lastUpdateTime < this.updateThrottleMs) {
225
+ this.pendingText = mergedInput;
226
+ if (!this.flushTimer) {
227
+ this.flushTimer = setTimeout(() => {
228
+ this.flushTimer = null;
229
+ const pending = this.pendingText;
230
+ this.pendingText = null;
231
+ if (pending) {
232
+ void this.update(pending).catch((error) => this.log?.("stream update failed", { error: String(error) }));
233
+ }
234
+ }, this.updateThrottleMs);
235
+ }
236
+ return;
237
+ }
238
+
239
+ this.pendingText = null;
240
+ this.lastUpdateTime = now;
241
+ this.queue = this.queue.then(async () => {
242
+ if (!this.state || this.closed) return;
243
+ const mergedText = mergeStreamingText(this.state.currentText, mergedInput);
244
+ if (!mergedText || mergedText === this.state.currentText) return;
245
+ this.state.currentText = mergedText;
246
+ this.log?.("stream update", { text: mergedText });
247
+ await this.updateCardContent(mergedText);
248
+ });
249
+ await this.queue;
250
+ }
251
+
252
+ async close(finalText?: string): Promise<void> {
253
+ if (!this.state || this.closed) return;
254
+ this.closed = true;
255
+ if (this.flushTimer) {
256
+ clearTimeout(this.flushTimer);
257
+ this.flushTimer = null;
258
+ }
259
+ await this.queue;
260
+
261
+ const text = finalText ? mergeStreamingText(this.state.currentText, finalText) : this.state.currentText;
262
+ if (text && text !== this.state.currentText) {
263
+ await this.updateCardContent(text);
264
+ this.state.currentText = text;
265
+ }
266
+
267
+ this.state.sequence += 1;
268
+ const token = await getToken(this.creds);
269
+ const response = await fetch(`${resolveApiBase(this.creds.domain)}/cardkit/v1/cards/${this.state.cardId}/settings`, {
270
+ method: "PATCH",
271
+ headers: {
272
+ Authorization: `Bearer ${token}`,
273
+ "Content-Type": "application/json; charset=utf-8",
274
+ },
275
+ body: JSON.stringify({
276
+ settings: JSON.stringify({
277
+ config: { streaming_mode: false, summary: { content: truncateSummary(this.state.currentText) } },
278
+ }),
279
+ sequence: this.state.sequence,
280
+ uuid: `c_${this.state.cardId}_${this.state.sequence}`,
281
+ }),
282
+ });
283
+ if (!response.ok) {
284
+ const body = await readResponseBodySafe(response);
285
+ throw new Error(
286
+ `Close card request failed with HTTP ${response.status}${body ? `: ${body}` : ""}`,
287
+ );
288
+ }
289
+
290
+ this.state = null;
291
+ this.pendingText = null;
292
+ }
293
+
294
+ isActive(): boolean {
295
+ return this.state !== null && !this.closed;
296
+ }
297
+ }
@@ -0,0 +1,171 @@
1
+ import {
2
+ type BotMessage,
3
+ type BotReply,
4
+ type ChannelHandler,
5
+ type ChannelInstance,
6
+ type ChannelTransport,
7
+ createBotMessage,
8
+ normalizeReply,
9
+ shouldHandleGroupMessage
10
+ } from "../../core.js";
11
+
12
+ export interface WechatIncomingEvent {
13
+ message?: WechatIncomingEvent;
14
+ messageId?: string;
15
+ fromUser?: string;
16
+ roomId?: string;
17
+ conversationId?: string;
18
+ senderId?: string;
19
+ text?: string;
20
+ mentions?: string[];
21
+ threadId?: string;
22
+ }
23
+
24
+ export interface WechatOutgoingMessage extends BotReply {
25
+ channel: "wechat";
26
+ conversationId: string;
27
+ replyToMessageId: string;
28
+ }
29
+
30
+ export interface WechatChannelConfig {
31
+ routing?: {
32
+ groupRequireMention?: boolean;
33
+ };
34
+ transport?: ChannelTransport<WechatOutgoingMessage>;
35
+ }
36
+
37
+ export interface WechatHandleResult {
38
+ handled: boolean;
39
+ reason?: "ignored" | "filtered";
40
+ message?: BotMessage;
41
+ reply?: WechatOutgoingMessage | null;
42
+ }
43
+
44
+ export interface WechatChannel extends ChannelInstance {
45
+ name: "wechat";
46
+ isStarted(): boolean;
47
+ getSentMessages(): WechatOutgoingMessage[];
48
+ handleEvent(event: WechatIncomingEvent): Promise<WechatHandleResult>;
49
+ simulateIncomingText(input: {
50
+ text: string;
51
+ senderId?: string;
52
+ conversationId?: string;
53
+ isDirectMessage?: boolean;
54
+ mentions?: string[];
55
+ }): Promise<WechatHandleResult>;
56
+ }
57
+
58
+ function unwrapSource(event: WechatIncomingEvent): WechatIncomingEvent {
59
+ return event.message ?? event;
60
+ }
61
+
62
+ export function normalizeWechatEvent(event: WechatIncomingEvent): BotMessage | null {
63
+ const source = unwrapSource(event);
64
+ const roomId = source.roomId ?? source.conversationId;
65
+ const senderId = source.fromUser ?? source.senderId;
66
+ const text = source.text;
67
+
68
+ if (!senderId || typeof text !== "string") {
69
+ return null;
70
+ }
71
+
72
+ return createBotMessage({
73
+ channel: "wechat",
74
+ conversationId: roomId ?? senderId,
75
+ senderId,
76
+ messageId: source.messageId ?? `wechat-${Date.now()}`,
77
+ text,
78
+ isDirectMessage: !roomId,
79
+ mentions: source.mentions ?? [],
80
+ threadId: source.threadId,
81
+ raw: event
82
+ });
83
+ }
84
+
85
+ function createOutgoingMessage(message: BotMessage, reply: BotReply): WechatOutgoingMessage {
86
+ return {
87
+ channel: "wechat",
88
+ conversationId: message.conversationId,
89
+ replyToMessageId: message.messageId,
90
+ text: reply.text
91
+ };
92
+ }
93
+
94
+ export function createWechatChannel(
95
+ config: WechatChannelConfig = {},
96
+ handler: ChannelHandler = {}
97
+ ): WechatChannel {
98
+ const sentMessages: WechatOutgoingMessage[] = [];
99
+ let started = false;
100
+
101
+ async function sendReply(
102
+ message: BotMessage,
103
+ reply: string | BotReply | null | void
104
+ ): Promise<WechatOutgoingMessage | null> {
105
+ const normalizedReply = normalizeReply(reply);
106
+ if (!normalizedReply) return null;
107
+
108
+ const outgoingMessage = createOutgoingMessage(message, normalizedReply);
109
+ sentMessages.push(outgoingMessage);
110
+
111
+ await config.transport?.send?.(outgoingMessage);
112
+ return outgoingMessage;
113
+ }
114
+
115
+ return {
116
+ name: "wechat",
117
+ async start() {
118
+ started = true;
119
+ await config.transport?.start?.();
120
+ },
121
+ async stop() {
122
+ started = false;
123
+ await config.transport?.stop?.();
124
+ },
125
+ isStarted() {
126
+ return started;
127
+ },
128
+ getSentMessages() {
129
+ return [...sentMessages];
130
+ },
131
+ async handleEvent(event) {
132
+ const message = normalizeWechatEvent(event);
133
+ if (!message) {
134
+ return { handled: false, reason: "ignored" };
135
+ }
136
+
137
+ const shouldHandle = shouldHandleGroupMessage(message, {
138
+ requireMention: config.routing?.groupRequireMention ?? false
139
+ });
140
+
141
+ if (!shouldHandle) {
142
+ return { handled: false, reason: "filtered", message };
143
+ }
144
+
145
+ const reply = await handler.onMessage?.(message);
146
+ const outgoingMessage = await sendReply(message, reply);
147
+
148
+ return {
149
+ handled: true,
150
+ message,
151
+ reply: outgoingMessage
152
+ };
153
+ },
154
+ async simulateIncomingText({
155
+ text,
156
+ senderId = "wechat-user",
157
+ conversationId = "wechat-chat",
158
+ isDirectMessage = true,
159
+ mentions = []
160
+ }) {
161
+ return this.handleEvent({
162
+ messageId: `wechat-${Date.now()}`,
163
+ fromUser: senderId,
164
+ roomId: isDirectMessage ? undefined : conversationId,
165
+ conversationId,
166
+ text,
167
+ mentions
168
+ });
169
+ }
170
+ };
171
+ }
@@ -0,0 +1,117 @@
1
+ import { getDefaultConfigPath, getNested, setNested, deleteNested } from "./config.js";
2
+ import { createFileConfigStore, type ConfigStore } from "./dashboard/config-store.js";
3
+
4
+ function coerceValue(raw: string): unknown {
5
+ const trimmed = raw.trim();
6
+ if (trimmed === "true") return true;
7
+ if (trimmed === "false") return false;
8
+ if (trimmed === "null") return null;
9
+ if (trimmed !== "" && /^-?(?:\d+\.?\d*|\.\d+)$/.test(trimmed)) return Number(trimmed);
10
+ return raw;
11
+ }
12
+
13
+ function formatOutput(value: unknown): string {
14
+ if (value === undefined) return "";
15
+ if (value === null || typeof value === "boolean" || typeof value === "number") {
16
+ return String(value);
17
+ }
18
+ if (typeof value === "string") return value;
19
+ return JSON.stringify(value, null, 2);
20
+ }
21
+
22
+ export interface CliContext {
23
+ configStore: ConfigStore;
24
+ log: (message: string) => void;
25
+ error: (message: string) => void;
26
+ }
27
+
28
+ function printUsage(ctx: CliContext) {
29
+ ctx.log("Usage:");
30
+ ctx.log(" config set <key-path> <value> Set a config value");
31
+ ctx.log(" config get <key-path> Get a config value");
32
+ ctx.log(" config list Show full config");
33
+ ctx.log(" config delete <key-path> Delete a config key");
34
+ }
35
+
36
+ export function runConfigCommand(args: string[], ctx: CliContext): boolean {
37
+ if (args[0] !== "config") return false;
38
+
39
+ const subcommand = args[1];
40
+
41
+ if (!subcommand) {
42
+ printUsage(ctx);
43
+ return true;
44
+ }
45
+
46
+ switch (subcommand) {
47
+ case "set": {
48
+ const keyPath = args[2];
49
+ const rawValue = args[3];
50
+ if (!keyPath || rawValue === undefined) {
51
+ ctx.error("Usage: config set <key-path> <value>");
52
+ return true;
53
+ }
54
+ const store = ctx.configStore;
55
+ const root = store.read();
56
+ const value = coerceValue(rawValue);
57
+ setNested(root, keyPath.split("."), value);
58
+ store.write(root);
59
+ ctx.log(`Set ${keyPath} = ${formatOutput(value)}`);
60
+ return true;
61
+ }
62
+ case "get": {
63
+ const keyPath = args[2];
64
+ if (!keyPath) {
65
+ ctx.error("Usage: config get <key-path>");
66
+ return true;
67
+ }
68
+ const root = ctx.configStore.read();
69
+ const value = getNested(root, keyPath.split("."));
70
+ if (value === undefined) {
71
+ ctx.error(`Key "${keyPath}" not found.`);
72
+ } else {
73
+ ctx.log(formatOutput(value));
74
+ }
75
+ return true;
76
+ }
77
+ case "list": {
78
+ const root = ctx.configStore.read();
79
+ ctx.log(JSON.stringify(root, null, 2));
80
+ return true;
81
+ }
82
+ case "delete": {
83
+ const keyPath = args[2];
84
+ if (!keyPath) {
85
+ ctx.error("Usage: config delete <key-path>");
86
+ return true;
87
+ }
88
+ const store = ctx.configStore;
89
+ const root = store.read();
90
+ const deleted = deleteNested(root, keyPath.split("."));
91
+ if (deleted) {
92
+ store.write(root);
93
+ ctx.log(`Deleted ${keyPath}`);
94
+ } else {
95
+ ctx.error(`Key "${keyPath}" not found.`);
96
+ }
97
+ return true;
98
+ }
99
+ default:
100
+ ctx.error(`Unknown config subcommand: ${subcommand}`);
101
+ printUsage(ctx);
102
+ return true;
103
+ }
104
+ }
105
+
106
+ export function runCli(args: string[]): boolean {
107
+ if (args[0] !== "config") return false;
108
+
109
+ const configPath = getDefaultConfigPath();
110
+ const configStore = createFileConfigStore(configPath);
111
+
112
+ return runConfigCommand(args, {
113
+ configStore,
114
+ log: (message) => console.log(message),
115
+ error: (message) => console.error(message),
116
+ });
117
+ }