| 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 |