返回 CodeWhale
lib.mjs
1 import {
2 activeTurnBlock,
3 cleanEnvValue,
4 commandAction as coreCommandAction,
5 compactRuntimeError,
6 isPlaceholderValue,
7 latestRunningTurn,
8 parseApprovalDecisionArgs,
9 parseBool,
10 parseCommand,
11 parseList,
12 parseTextContent as coreParseTextContent,
13 preservedChatStateFields,
14 splitMessage,
15 stripGroupPrefix as coreStripGroupPrefix,
16 ThreadStore as CoreThreadStore
17 } from "../../bridge-core/src/lib.mjs";
18
19 export {
20 activeTurnBlock,
21 cleanEnvValue,
22 compactRuntimeError,
23 isPlaceholderValue,
24 latestRunningTurn,
25 parseApprovalDecisionArgs,
26 parseBool,
27 parseCommand,
28 parseList,
29 preservedChatStateFields,
30 splitMessage
31 };
32
33 export function requiredEnv(name) {
34 const value = process.env[name];
35 if (!value || !value.trim()) {
36 throw new Error(`${name} is required`);
37 }
38 return value.trim();
39 }
40
41 export function parseTextContent(content) {
42 return coreParseTextContent(content, ["text"]);
43 }
44
45 export function incomingIdentity(body) {
46 const from = body?.from || {};
47 const chatId = body.chatid || (body.chattype === "single" && from.userid ? `single:${from.userid}` : "");
48 return {
49 chatId,
50 messageId: body.msgid || "",
51 chatType: body.chattype || "single",
52 userId: from.userid || "",
53 aibotId: body.aibotid || ""
54 };
55 }
56
57 export function isAllowed(identity, allowlist, allowUnlisted = false) {
58 if (allowUnlisted) return true;
59 const allowed = new Set(allowlist);
60 return [identity.chatId, identity.userId].filter(Boolean).some((id) => allowed.has(id));
61 }
62
63 export function pairingRefusalText(identity) {
64 return [
65 "This chat is not in WECOM_CHAT_ALLOWLIST.",
66 `chat_id=${identity.chatId}`,
67 identity.userId ? `user_id=${identity.userId}` : ""
68 ]
69 .filter(Boolean)
70 .join("\n");
71 }
72
73 export function stripGroupPrefix(text, { chatType, requirePrefix, prefix }) {
74 return coreStripGroupPrefix(text, {
75 chatType,
76 requirePrefix,
77 prefix: prefix || "/ds",
78 directChatTypes: ["single"]
79 });
80 }
81
82 /** Check if text is a natural-language approval response (Chinese or English). */
83 export function isApprovalResponse(text) {
84 const t = String(text || "").trim().toLowerCase();
85 // Single-word approvals
86 if (["允许", "可以", "好", "同意", "批准", "yes", "ok", "y", "approve", "allow"].includes(t)) return true;
87 // Two-char approvals
88 if (["好的", "可以", "没问题", "批准了", "同意"].includes(t)) return true;
89 return false;
90 }
91
92 /** Check if text is a natural-language deny response. */
93 export function isDenyResponse(text) {
94 const t = String(text || "").trim().toLowerCase();
95 if (["拒绝", "不行", "不要", "no", "n", "deny", "reject", "取消", "stop", "否"].includes(t)) return true;
96 if (["不可以", "不同意", "不要执行"].includes(t)) return true;
97 return false;
98 }
99
100 export function commandAction(command) {
101 return coreCommandAction(command);
102 }
103
104 export function helpText() {
105 return [
106 "CodeWhale 企业微信桥接命令:",
107 "/help - 显示帮助",
108 "/status - runtime 和工作区状态",
109 "/threads - 最近的 runtime 线程",
110 "/new - 为此聊天创建新线程",
111 "/resume <thread_id> - 绑定到此聊天的现有线程",
112 "/model <name|default> - 设置或重置聊天模型",
113 "/interrupt - 中断活动 turn",
114 "/compact - 压缩当前线程",
115 "/allow <approval_id> [remember] - 批准待处理的工具调用",
116 "/deny <approval_id> - 拒绝待处理的工具调用",
117 "",
118 "其他所有内容均作为 CodeWhale 提示发送。"
119 ].join("\n");
120 }
121
122 export class ThreadStore extends CoreThreadStore {
123 constructor(filePath) {
124 super(filePath, { privateMode: true });
125 }
126 }
127
128 export function validateBridgeConfig(env) {
129 const errors = [];
130 const warnings = [];
131 const info = [];
132 const add = (list, code, message) => list.push({ code, message });
133
134 for (const key of ["WECOM_BOT_ID", "WECOM_BOT_SECRET"]) {
135 const value = cleanEnvValue(env[key]);
136 if (!value) {
137 add(errors, "missing_required", `${key} is required`);
138 } else if (isPlaceholderValue(value)) {
139 add(errors, "placeholder_value", `${key} still contains a placeholder value`);
140 }
141 }
142
143 const runtimeUrl = cleanEnvValue(env.CODEWHALE_RUNTIME_URL || "http://127.0.0.1:7878");
144 try {
145 const parsed = new URL(runtimeUrl);
146 if (!["http:", "https:"].includes(parsed.protocol)) {
147 add(errors, "invalid_runtime_url", "CODEWHALE_RUNTIME_URL must use http or https");
148 }
149 } catch {
150 add(errors, "invalid_runtime_url", "CODEWHALE_RUNTIME_URL is not a valid URL");
151 }
152
153 const runtimeToken = cleanEnvValue(env.CODEWHALE_RUNTIME_TOKEN);
154 if (!runtimeToken) {
155 add(errors, "missing_runtime_token", "CODEWHALE_RUNTIME_TOKEN is required");
156 } else if (isPlaceholderValue(runtimeToken)) {
157 add(errors, "placeholder_runtime_token", "CODEWHALE_RUNTIME_TOKEN is still a placeholder");
158 }
159
160 const allowUnlisted = parseBool(env.WECOM_ALLOW_UNLISTED, false);
161 const allowlist = parseList(env.WECOM_CHAT_ALLOWLIST);
162
163 if (!allowlist.length && allowUnlisted) {
164 add(warnings, "pairing_mode_open", "WECOM_ALLOW_UNLISTED=true leaves first-pairing mode open");
165 } else if (!allowlist.length) {
166 add(warnings, "not_paired", "WECOM_CHAT_ALLOWLIST is empty; all chats will be refused");
167 }
168
169 return { ok: errors.length === 0, errors, warnings, info };
170 }
171
172 export function formatValidationReport(result) {
173 const lines = ["WeCom bridge config validation"];
174 for (const item of result.errors) lines.push(`[fail] ${item.message}`);
175 for (const item of result.warnings) lines.push(`[warn] ${item.message}`);
176 for (const item of result.info) lines.push(`[info] ${item.message}`);
177 if (result.ok) lines.push("[ok] No blocking config errors found");
178 return lines.join("\n");
179 }
180
180 lines Plain Text