@coze-arch/cli 0.0.18 → 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.
- package/lib/__templates__/expo/.coze +1 -0
- package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
- package/lib/__templates__/expo/package.json +2 -1
- package/lib/__templates__/nextjs/.coze +1 -0
- package/lib/__templates__/nextjs/package.json +3 -1
- package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
- package/lib/__templates__/nuxt-vue/.coze +1 -0
- package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
- package/lib/__templates__/nuxt-vue/package.json +9 -2
- package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
- package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
- package/lib/__templates__/pi-agent/.coze +10 -0
- package/lib/__templates__/pi-agent/AGENTS.md +144 -0
- package/lib/__templates__/pi-agent/README.md +216 -0
- package/lib/__templates__/pi-agent/_gitignore +3 -0
- package/lib/__templates__/pi-agent/_npmrc +23 -0
- package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
- package/lib/__templates__/pi-agent/docs/project-overview.md +374 -0
- package/lib/__templates__/pi-agent/docs/user/getting-started.md +47 -0
- package/lib/__templates__/pi-agent/package.json +63 -0
- package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
- package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +36 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +41 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +85 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +53 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +9 -0
- package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
- package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
- package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
- package/lib/__templates__/pi-agent/src/agent.ts +363 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
- package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
- package/lib/__templates__/pi-agent/src/cli.ts +117 -0
- package/lib/__templates__/pi-agent/src/config.ts +708 -0
- package/lib/__templates__/pi-agent/src/core.ts +218 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
- package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
- package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
- package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
- package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
- package/lib/__templates__/pi-agent/src/index.ts +123 -0
- package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
- package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
- package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
- package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
- package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
- package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
- package/lib/__templates__/pi-agent/template.config.js +45 -0
- package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
- package/lib/__templates__/pi-agent/tests/config.test.ts +315 -0
- package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
- package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
- package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
- package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
- package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
- package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
- package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
- package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
- package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
- package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
- package/lib/__templates__/pi-agent/tsconfig.json +21 -0
- package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
- package/lib/__templates__/taro/.coze +1 -0
- package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
- package/lib/__templates__/taro/package.json +1 -1
- package/lib/__templates__/templates.json +24 -0
- package/lib/__templates__/vite/.coze +1 -0
- package/lib/__templates__/vite/package.json +3 -1
- package/lib/__templates__/vite/scripts/validate.sh +10 -0
- package/lib/cli.js +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export type ChannelName = "feishu" | "wechat" | "dashboard";
|
|
2
|
+
|
|
3
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
4
|
+
|
|
5
|
+
export type AgentMode = "mock" | "pi";
|
|
6
|
+
|
|
7
|
+
export interface BotMessage {
|
|
8
|
+
// A channel package must normalize platform-specific payloads into this shape
|
|
9
|
+
// before handing control to the app runtime.
|
|
10
|
+
channel: ChannelName;
|
|
11
|
+
conversationId: string;
|
|
12
|
+
senderId: string;
|
|
13
|
+
messageId: string;
|
|
14
|
+
text: string;
|
|
15
|
+
isDirectMessage: boolean;
|
|
16
|
+
mentions: string[];
|
|
17
|
+
threadId?: string;
|
|
18
|
+
raw?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BotReply {
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ChannelReply = string | BotReply | null | void;
|
|
26
|
+
|
|
27
|
+
export interface ChannelStreamHandlers {
|
|
28
|
+
onMeta?(meta: { sessionKey: string }): void;
|
|
29
|
+
onDelta?(delta: string): void;
|
|
30
|
+
onError?(error: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ChannelHandler {
|
|
34
|
+
onMessage?(message: BotMessage): Promise<ChannelReply> | ChannelReply;
|
|
35
|
+
onStreamMessage?(
|
|
36
|
+
message: BotMessage,
|
|
37
|
+
handlers: ChannelStreamHandlers
|
|
38
|
+
): Promise<ChannelReply> | ChannelReply;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChannelInstance {
|
|
42
|
+
name: ChannelName;
|
|
43
|
+
start(): Promise<void>;
|
|
44
|
+
stop(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ChannelTransport<TOutgoingMessage> {
|
|
48
|
+
start?(): Promise<void> | void;
|
|
49
|
+
stop?(): Promise<void> | void;
|
|
50
|
+
send?(message: TOutgoingMessage): Promise<void> | void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RoutingGateConfig {
|
|
54
|
+
requireMention?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SessionStore<TValue> {
|
|
58
|
+
get(key: string): TValue | undefined;
|
|
59
|
+
set(key: string, value: TValue): TValue;
|
|
60
|
+
has(key: string): boolean;
|
|
61
|
+
delete(key: string): boolean;
|
|
62
|
+
clear(): void;
|
|
63
|
+
entries(): Array<[string, TValue]>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AgentRuntime {
|
|
67
|
+
// App projects own the agent runtime on purpose. We keep it out of channel/core
|
|
68
|
+
// so future work can use raw pi-coding-agent APIs directly.
|
|
69
|
+
run(message: BotMessage): Promise<string>;
|
|
70
|
+
dispose(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AgentRuntimeConfig {
|
|
74
|
+
mode: AgentMode;
|
|
75
|
+
provider?: string;
|
|
76
|
+
model?: string;
|
|
77
|
+
configPath?: string;
|
|
78
|
+
thinkingLevel?: ThinkingLevel;
|
|
79
|
+
baseUrl?: string;
|
|
80
|
+
cwd?: string;
|
|
81
|
+
agentDir?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface BotChannelConfig {
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface FeishuBotChannelConfig extends BotChannelConfig {
|
|
89
|
+
appId?: string;
|
|
90
|
+
appSecret?: string;
|
|
91
|
+
domain?: string;
|
|
92
|
+
encryptKey?: string;
|
|
93
|
+
verificationToken?: string;
|
|
94
|
+
thinkingReaction?: {
|
|
95
|
+
enabled?: boolean;
|
|
96
|
+
emojiType?: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface WechatBotChannelConfig extends BotChannelConfig {
|
|
101
|
+
implementation?: "mock";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BotRoutingConfig {
|
|
105
|
+
feishuGroupRequireMention: boolean;
|
|
106
|
+
wechatGroupRequireMention: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface BotAppConfig {
|
|
110
|
+
appName: string;
|
|
111
|
+
agent: AgentRuntimeConfig;
|
|
112
|
+
routing: BotRoutingConfig;
|
|
113
|
+
channels: {
|
|
114
|
+
feishu?: FeishuBotChannelConfig;
|
|
115
|
+
wechat?: WechatBotChannelConfig;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizeReply(reply: ChannelReply): BotReply | null {
|
|
120
|
+
if (reply == null) return null;
|
|
121
|
+
if (typeof reply === "string") return { text: reply };
|
|
122
|
+
if (typeof reply === "object" && typeof reply.text === "string") {
|
|
123
|
+
return { text: reply.text };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new TypeError("Reply must be a string, an object with a text field, or null.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function assertBotMessage(message: unknown): asserts message is BotMessage {
|
|
130
|
+
if (!message || typeof message !== "object") {
|
|
131
|
+
throw new TypeError("BotMessage must be an object.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const requiredFields: Array<keyof BotMessage> = [
|
|
135
|
+
"channel",
|
|
136
|
+
"conversationId",
|
|
137
|
+
"senderId",
|
|
138
|
+
"messageId",
|
|
139
|
+
"text",
|
|
140
|
+
"isDirectMessage",
|
|
141
|
+
"mentions"
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const field of requiredFields) {
|
|
145
|
+
if (!(field in message)) {
|
|
146
|
+
throw new TypeError(`BotMessage is missing required field: ${field}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getSessionKey(message: BotMessage): string {
|
|
152
|
+
assertBotMessage(message);
|
|
153
|
+
|
|
154
|
+
// Session routing is deterministic and host-controlled.
|
|
155
|
+
// The model never decides where a message should be threaded.
|
|
156
|
+
if (message.isDirectMessage) {
|
|
157
|
+
return `${message.channel}:dm:${message.senderId}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (message.threadId) {
|
|
161
|
+
return `${message.channel}:thread:${message.conversationId}:${message.threadId}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return `${message.channel}:group:${message.conversationId}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function shouldHandleGroupMessage(
|
|
168
|
+
message: BotMessage,
|
|
169
|
+
routing: RoutingGateConfig = {}
|
|
170
|
+
): boolean {
|
|
171
|
+
assertBotMessage(message);
|
|
172
|
+
|
|
173
|
+
// Mention gating lives in the host/channel layer, not in the model prompt.
|
|
174
|
+
if (message.isDirectMessage) return true;
|
|
175
|
+
if (!routing.requireMention) return true;
|
|
176
|
+
|
|
177
|
+
return Array.isArray(message.mentions) && message.mentions.length > 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createInMemorySessionStore<TValue>(): SessionStore<TValue> {
|
|
181
|
+
const sessions = new Map<string, TValue>();
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
get(key) {
|
|
185
|
+
return sessions.get(key);
|
|
186
|
+
},
|
|
187
|
+
set(key, value) {
|
|
188
|
+
sessions.set(key, value);
|
|
189
|
+
return value;
|
|
190
|
+
},
|
|
191
|
+
has(key) {
|
|
192
|
+
return sessions.has(key);
|
|
193
|
+
},
|
|
194
|
+
delete(key) {
|
|
195
|
+
return sessions.delete(key);
|
|
196
|
+
},
|
|
197
|
+
clear() {
|
|
198
|
+
sessions.clear();
|
|
199
|
+
},
|
|
200
|
+
entries() {
|
|
201
|
+
return Array.from(sessions.entries());
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createBotMessage(overrides: Partial<BotMessage> = {}): BotMessage {
|
|
207
|
+
return {
|
|
208
|
+
channel: overrides.channel ?? "feishu",
|
|
209
|
+
conversationId: overrides.conversationId ?? "conversation-1",
|
|
210
|
+
senderId: overrides.senderId ?? "user-1",
|
|
211
|
+
messageId: overrides.messageId ?? `msg-${Date.now()}`,
|
|
212
|
+
text: overrides.text ?? "",
|
|
213
|
+
isDirectMessage: overrides.isDirectMessage ?? true,
|
|
214
|
+
mentions: overrides.mentions ?? [],
|
|
215
|
+
threadId: overrides.threadId,
|
|
216
|
+
raw: overrides.raw
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfigStore,
|
|
3
|
+
type ConfigSource,
|
|
4
|
+
} from "../config-store.js";
|
|
5
|
+
import { updateString, updateBool } from "../../config.js";
|
|
6
|
+
|
|
7
|
+
export type DashboardChannelsResponse = {
|
|
8
|
+
routing: {
|
|
9
|
+
feishuGroupRequireMention: boolean;
|
|
10
|
+
wechatGroupRequireMention: boolean;
|
|
11
|
+
};
|
|
12
|
+
feishu: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
requireMention?: boolean;
|
|
15
|
+
appId?: string;
|
|
16
|
+
domain?: string;
|
|
17
|
+
encryptKey?: string;
|
|
18
|
+
verificationToken?: string;
|
|
19
|
+
appSecret?: string;
|
|
20
|
+
thinkingReaction?: {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
emojiType?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
wechat: {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
requireMention?: boolean;
|
|
28
|
+
implementation?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type DashboardChannelsSaveRequest = {
|
|
33
|
+
channels: DashboardChannelsResponse;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function readChannelsResponse(args: ConfigSource): DashboardChannelsResponse {
|
|
37
|
+
const root = getConfigStore(args).read();
|
|
38
|
+
const cfg = root as {
|
|
39
|
+
channels?: { feishu?: Record<string, unknown>; wechat?: Record<string, unknown> };
|
|
40
|
+
};
|
|
41
|
+
const feishuRaw = cfg.channels?.feishu ?? {};
|
|
42
|
+
const wechatRaw = cfg.channels?.wechat ?? {};
|
|
43
|
+
|
|
44
|
+
const feishuEnabled = Boolean((feishuRaw as { enabled?: unknown }).enabled);
|
|
45
|
+
const wechatEnabled = Boolean((wechatRaw as { enabled?: unknown }).enabled);
|
|
46
|
+
const feishuRequireMention = Boolean((feishuRaw as { requireMention?: unknown }).requireMention);
|
|
47
|
+
const wechatRequireMention = Boolean((wechatRaw as { requireMention?: unknown }).requireMention);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
routing: {
|
|
51
|
+
feishuGroupRequireMention: feishuRequireMention,
|
|
52
|
+
wechatGroupRequireMention: wechatRequireMention,
|
|
53
|
+
},
|
|
54
|
+
feishu: {
|
|
55
|
+
enabled: feishuEnabled,
|
|
56
|
+
requireMention: feishuRequireMention,
|
|
57
|
+
appId: (feishuRaw as { appId?: string }).appId,
|
|
58
|
+
domain: (feishuRaw as { domain?: string }).domain,
|
|
59
|
+
appSecret: (feishuRaw as { appSecret?: string }).appSecret,
|
|
60
|
+
encryptKey: (feishuRaw as { encryptKey?: string }).encryptKey,
|
|
61
|
+
verificationToken: (feishuRaw as { verificationToken?: string }).verificationToken,
|
|
62
|
+
thinkingReaction: (feishuRaw as { thinkingReaction?: unknown }).thinkingReaction as
|
|
63
|
+
| { enabled?: boolean; emojiType?: string }
|
|
64
|
+
| undefined,
|
|
65
|
+
},
|
|
66
|
+
wechat: {
|
|
67
|
+
enabled: wechatEnabled,
|
|
68
|
+
requireMention: wechatRequireMention,
|
|
69
|
+
implementation: (wechatRaw as { implementation?: string }).implementation,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function saveChannelsRequest(args: ConfigSource & { body: DashboardChannelsSaveRequest }) {
|
|
75
|
+
const { channels } = args.body;
|
|
76
|
+
const configStore = getConfigStore(args);
|
|
77
|
+
const root = configStore.read();
|
|
78
|
+
|
|
79
|
+
updateBool(root, ["channels", "feishu", "enabled"], channels.feishu.enabled);
|
|
80
|
+
updateBool(root, ["channels", "feishu", "requireMention"], channels.feishu.requireMention);
|
|
81
|
+
updateString(root, ["channels", "feishu", "appId"], channels.feishu.appId);
|
|
82
|
+
updateString(root, ["channels", "feishu", "domain"], channels.feishu.domain);
|
|
83
|
+
updateString(root, ["channels", "feishu", "appSecret"], channels.feishu.appSecret);
|
|
84
|
+
updateString(root, ["channels", "feishu", "encryptKey"], channels.feishu.encryptKey);
|
|
85
|
+
updateString(root, ["channels", "feishu", "verificationToken"], channels.feishu.verificationToken);
|
|
86
|
+
if (channels.feishu.thinkingReaction) {
|
|
87
|
+
updateBool(
|
|
88
|
+
root,
|
|
89
|
+
["channels", "feishu", "thinkingReaction", "enabled"],
|
|
90
|
+
channels.feishu.thinkingReaction.enabled,
|
|
91
|
+
);
|
|
92
|
+
updateString(
|
|
93
|
+
root,
|
|
94
|
+
["channels", "feishu", "thinkingReaction", "emojiType"],
|
|
95
|
+
channels.feishu.thinkingReaction.emojiType,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
updateBool(root, ["channels", "wechat", "enabled"], channels.wechat.enabled);
|
|
100
|
+
updateBool(root, ["channels", "wechat", "requireMention"], channels.wechat.requireMention);
|
|
101
|
+
updateString(root, ["channels", "wechat", "implementation"], channels.wechat.implementation);
|
|
102
|
+
|
|
103
|
+
configStore.write(root);
|
|
104
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type DashboardDocSummary = {
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
group: string;
|
|
9
|
+
order: number;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DashboardDocDetail = DashboardDocSummary & {
|
|
14
|
+
content: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DashboardDocsResponse = {
|
|
18
|
+
docs: DashboardDocSummary[];
|
|
19
|
+
selectedDoc: DashboardDocDetail | null;
|
|
20
|
+
requestedSlug?: string;
|
|
21
|
+
requestedSlugFound: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ParsedFrontmatter = {
|
|
25
|
+
title?: string;
|
|
26
|
+
summary?: string;
|
|
27
|
+
group?: string;
|
|
28
|
+
order?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function listMarkdownFiles(rootDir: string): string[] {
|
|
32
|
+
if (!existsSync(rootDir)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const entries = readdirSync(rootDir, { withFileTypes: true }).sort((left, right) =>
|
|
37
|
+
left.name.localeCompare(right.name)
|
|
38
|
+
);
|
|
39
|
+
const files: string[] = [];
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const fullPath = join(rootDir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
files.push(...listMarkdownFiles(fullPath));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
48
|
+
files.push(fullPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stripQuotes(value: string): string {
|
|
56
|
+
if (
|
|
57
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
58
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
59
|
+
) {
|
|
60
|
+
return value.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseFrontmatter(raw: string): { meta: ParsedFrontmatter; content: string } {
|
|
66
|
+
const normalized = raw.replace(/\r\n/g, "\n");
|
|
67
|
+
if (!normalized.startsWith("---\n")) {
|
|
68
|
+
return { meta: {}, content: raw };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = normalized.split("\n");
|
|
72
|
+
let closingIndex = -1;
|
|
73
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
74
|
+
if (lines[index]?.trim() === "---") {
|
|
75
|
+
closingIndex = index;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (closingIndex === -1) {
|
|
81
|
+
return { meta: {}, content: raw };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const meta: ParsedFrontmatter = {};
|
|
85
|
+
for (const line of lines.slice(1, closingIndex)) {
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
88
|
+
|
|
89
|
+
const match = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
90
|
+
if (!match) continue;
|
|
91
|
+
|
|
92
|
+
const key = match[1]!;
|
|
93
|
+
const value = stripQuotes(match[2]!.trim());
|
|
94
|
+
if (!value) continue;
|
|
95
|
+
|
|
96
|
+
if (key === "order") {
|
|
97
|
+
const parsed = Number(value);
|
|
98
|
+
if (Number.isFinite(parsed)) {
|
|
99
|
+
meta.order = parsed;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (key === "title") meta.title = value;
|
|
105
|
+
if (key === "summary") meta.summary = value;
|
|
106
|
+
if (key === "group") meta.group = value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
meta,
|
|
111
|
+
content: lines.slice(closingIndex + 1).join("\n").trimStart(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractHeading(content: string): string {
|
|
116
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const match = line.trim().match(/^#\s+(.+)$/);
|
|
119
|
+
if (match) {
|
|
120
|
+
return match[1]!.trim();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractSummary(content: string): string {
|
|
127
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
128
|
+
const paragraph: string[] = [];
|
|
129
|
+
let inCodeBlock = false;
|
|
130
|
+
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (!trimmed) {
|
|
134
|
+
if (paragraph.length > 0) break;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (trimmed.startsWith("```")) {
|
|
138
|
+
inCodeBlock = !inCodeBlock;
|
|
139
|
+
if (paragraph.length > 0) break;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (inCodeBlock) continue;
|
|
143
|
+
if (trimmed.startsWith("#")) continue;
|
|
144
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || /^\d+\.\s/.test(trimmed)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
paragraph.push(trimmed.replace(/^>\s?/, ""));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return paragraph.join(" ").trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createTitleFromSlug(slug: string): string {
|
|
155
|
+
const tail = slug.split("/").at(-1) ?? slug;
|
|
156
|
+
return tail
|
|
157
|
+
.split(/[-_]/g)
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
160
|
+
.join(" ");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadDoc(filePath: string, docsDir: string): DashboardDocDetail {
|
|
164
|
+
const relativePath = relative(docsDir, filePath).split(sep).join("/");
|
|
165
|
+
const slug = relativePath.replace(/\.md$/i, "");
|
|
166
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
167
|
+
const parsed = parseFrontmatter(raw);
|
|
168
|
+
const title = parsed.meta.title?.trim() || extractHeading(parsed.content) || createTitleFromSlug(slug);
|
|
169
|
+
const summary = parsed.meta.summary?.trim() || extractSummary(parsed.content);
|
|
170
|
+
const group = parsed.meta.group?.trim() || "其他";
|
|
171
|
+
const order = parsed.meta.order ?? Number.MAX_SAFE_INTEGER;
|
|
172
|
+
const updatedAt = statSync(filePath).mtime.toISOString();
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
slug,
|
|
176
|
+
title,
|
|
177
|
+
summary,
|
|
178
|
+
group,
|
|
179
|
+
order,
|
|
180
|
+
updatedAt,
|
|
181
|
+
content: parsed.content,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function readDocsResponse(args: { docsDir: string; slug?: string }): DashboardDocsResponse {
|
|
186
|
+
const docs = listMarkdownFiles(args.docsDir)
|
|
187
|
+
.map((filePath) => loadDoc(filePath, args.docsDir))
|
|
188
|
+
.sort((left, right) => {
|
|
189
|
+
if (left.order !== right.order) return left.order - right.order;
|
|
190
|
+
return left.title.localeCompare(right.title);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const requestedSlug = args.slug?.trim();
|
|
194
|
+
const selectedDoc = requestedSlug
|
|
195
|
+
? docs.find((doc) => doc.slug === requestedSlug) ?? docs[0] ?? null
|
|
196
|
+
: docs[0] ?? null;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
docs: docs.map(({ content: _content, ...summary }) => summary),
|
|
200
|
+
selectedDoc,
|
|
201
|
+
requestedSlug,
|
|
202
|
+
requestedSlugFound: requestedSlug ? selectedDoc?.slug === requestedSlug : true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfigStore,
|
|
3
|
+
type ConfigSource,
|
|
4
|
+
} from "../config-store.js";
|
|
5
|
+
import { ensureObject, readString, updateString } from "../../config.js";
|
|
6
|
+
|
|
7
|
+
export type DashboardModelOption = {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type DashboardModelsResponse = {
|
|
13
|
+
defaultModel: string;
|
|
14
|
+
options: DashboardModelOption[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DashboardModelsSaveRequest = {
|
|
18
|
+
models: {
|
|
19
|
+
defaultModel: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class ValidationError extends Error {
|
|
24
|
+
statusCode = 400 as const;
|
|
25
|
+
constructor(message: string) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "ValidationError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateDefaultModel(args: { defaultModel: string; allowed: Set<string> }) {
|
|
32
|
+
if (typeof args.defaultModel !== "string") {
|
|
33
|
+
throw new ValidationError("Invalid payload.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultModel = args.defaultModel.trim();
|
|
37
|
+
if (!defaultModel) return;
|
|
38
|
+
|
|
39
|
+
const parts = defaultModel.split("/");
|
|
40
|
+
if (parts.length !== 2) {
|
|
41
|
+
throw new ValidationError(`Invalid defaultModel: "${args.defaultModel}". Expected "provider/model".`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const providerId = parts[0] ?? "";
|
|
45
|
+
const modelId = parts[1] ?? "";
|
|
46
|
+
const idPattern = /^[A-Za-z0-9_.-]+$/;
|
|
47
|
+
if (!idPattern.test(providerId) || !idPattern.test(modelId)) {
|
|
48
|
+
throw new ValidationError(`Invalid defaultModel: "${args.defaultModel}". Expected "provider/model".`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!args.allowed.has(defaultModel)) {
|
|
52
|
+
throw new ValidationError(`Invalid defaultModel: "${args.defaultModel}" is not in the configured model list.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readModelsResponse(args: ConfigSource): DashboardModelsResponse {
|
|
57
|
+
const root = getConfigStore(args).read();
|
|
58
|
+
const agents = ensureObject(root.agents);
|
|
59
|
+
const defaults = ensureObject(agents.defaults);
|
|
60
|
+
const defaultModel = readString(ensureObject(defaults.model).primary);
|
|
61
|
+
const providersRoot = ensureObject(ensureObject(root.models).providers);
|
|
62
|
+
|
|
63
|
+
const options = Object.entries(providersRoot).flatMap(([providerId, providerValue]) => {
|
|
64
|
+
const provider = ensureObject(providerValue);
|
|
65
|
+
const rawModels = Array.isArray(provider.models) ? provider.models : [];
|
|
66
|
+
return rawModels.flatMap((rawModel) => {
|
|
67
|
+
const model = ensureObject(rawModel);
|
|
68
|
+
const id = readString(model.id);
|
|
69
|
+
if (!id) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const name = readString(model.name) || id;
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
value: `${providerId}/${id}`,
|
|
76
|
+
label: `${providerId} / ${name}`,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
defaultModel,
|
|
84
|
+
options,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function saveModelsRequest(args: ConfigSource & { body: DashboardModelsSaveRequest }) {
|
|
89
|
+
const configStore = getConfigStore(args);
|
|
90
|
+
const root = configStore.read();
|
|
91
|
+
const current = readModelsResponse({ configStore });
|
|
92
|
+
const allowed = new Set(current.options.map((option) => option.value));
|
|
93
|
+
validateDefaultModel({ defaultModel: args.body.models.defaultModel, allowed });
|
|
94
|
+
|
|
95
|
+
updateString(root, ["agents", "defaults", "model", "primary"], args.body.models.defaultModel);
|
|
96
|
+
|
|
97
|
+
configStore.write(root);
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type DashboardOverviewResponse = {
|
|
2
|
+
appName: string;
|
|
3
|
+
botStatus: string;
|
|
4
|
+
dashboardUrl: string;
|
|
5
|
+
agentMode: string;
|
|
6
|
+
workspaceDir: string;
|
|
7
|
+
agentDir: string;
|
|
8
|
+
enabledChannels: Array<{ id: string; enabled: boolean }>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function buildOverviewResponse(args: {
|
|
12
|
+
dashboardUrl: string;
|
|
13
|
+
dashboardStarted: boolean;
|
|
14
|
+
appName: string;
|
|
15
|
+
agentMode: string;
|
|
16
|
+
workspaceDir: string;
|
|
17
|
+
agentDir: string;
|
|
18
|
+
enabledChannels: { feishu: boolean; wechat: boolean };
|
|
19
|
+
}): DashboardOverviewResponse {
|
|
20
|
+
const { dashboardUrl, dashboardStarted } = args;
|
|
21
|
+
return {
|
|
22
|
+
appName: args.appName,
|
|
23
|
+
botStatus: dashboardStarted ? "running" : "stopped",
|
|
24
|
+
dashboardUrl,
|
|
25
|
+
agentMode: args.agentMode,
|
|
26
|
+
workspaceDir: args.workspaceDir || "Not set",
|
|
27
|
+
agentDir: args.agentDir || "Not set",
|
|
28
|
+
enabledChannels: [
|
|
29
|
+
{ id: "feishu", enabled: Boolean(args.enabledChannels.feishu) },
|
|
30
|
+
{ id: "wechat", enabled: Boolean(args.enabledChannels.wechat) },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|