feat(wechat): add WX gate evidence and v1.49 103 cutover runbook
Prioritize scheduled-task intent over page.generate for automation phrases, add run-wechat-gate-evidence.mjs for WX-01..13, and document 103 maintenance-window steps for Goose v1.49. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* WeChat routing / intent evidence for v1.49 cutover (unit + contract checks; no live MP).
|
||||
* WeChat routing / intent evidence for v1.49 cutover.
|
||||
* Runs WX-01..WX-13 gate suites + wechat-intent-router; optional live webhook smoke.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
@@ -8,11 +9,13 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const skip = process.env.GOOSE_V149_WECHAT_EVIDENCE_SKIP === '1';
|
||||
const live = process.env.GOOSE_V149_WECHAT_LIVE_SMOKE === '1';
|
||||
|
||||
function runTest(label, file) {
|
||||
const result = spawnSync(process.execPath, ['--test', file], {
|
||||
function runScript(script, args = [], label = script) {
|
||||
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script), ...args], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${label} failed: ${result.stderr?.trim() || result.stdout?.trim()}`);
|
||||
@@ -25,11 +28,12 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
runTest('wechat-intent-router', 'wechat-intent-router.test.mjs');
|
||||
const gateArgs = live ? ['--live-only', '--live-scenario', 'poem-page'] : [];
|
||||
runScript('run-wechat-gate-evidence.mjs', gateArgs, 'wechat-gate-evidence');
|
||||
|
||||
const fs = await import('node:fs');
|
||||
const bundlePath = path.join(root, 'wechat-mp.bundle.mjs');
|
||||
const sourcePath = path.join(root, 'wechat-mp.mjs');
|
||||
const fs = await import('node:fs');
|
||||
const bundleExists = fs.existsSync(bundlePath);
|
||||
const sourceExists = fs.existsSync(sourcePath);
|
||||
if (!bundleExists && !sourceExists) {
|
||||
@@ -37,13 +41,13 @@ async function main() {
|
||||
}
|
||||
|
||||
console.log('GOOSE_V149_WECHAT_EVIDENCE_OK:');
|
||||
console.log(' unit=wechat-intent-router.test.mjs');
|
||||
console.log(' gate=WX-01..WX-13 deterministic suites');
|
||||
console.log(` live_smoke=${live ? 'poem-page' : 'not-run (set GOOSE_V149_WECHAT_LIVE_SMOKE=1)'}`);
|
||||
if (bundleExists) {
|
||||
console.log(` bundle=${bundlePath} (${fs.statSync(bundlePath).size} bytes)`);
|
||||
} else {
|
||||
console.log(` source=${sourcePath} (bundle not built; prod uses wechat-mp.bundle.mjs)`);
|
||||
}
|
||||
console.log(' note=live MP ACK/page-link E2E requires WeChat credentials; not run locally');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install Goose v1.49 (goose serve) as a local LaunchAgent on loopback :18049.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PORT="${GOOSE_V149_PORT:-18049}"
|
||||
LABEL="${GOOSE_V149_LAUNCHD_LABEL:-com.tkmind.local-goose-v149}"
|
||||
LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
||||
PLIST="${LAUNCHD_DIR}/${LABEL}.plist"
|
||||
RUN_SCRIPT="${ROOT}/scripts/run-goosed-v149-local.sh"
|
||||
LOG_ROOT="${GOOSE_V149_RUNTIME_ROOT:-/tmp/goose-v149-runtime}"
|
||||
GUI="gui/$(id -u)"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
bash scripts/install-local-goose-v149.sh
|
||||
bash scripts/install-local-goose-v149.sh --uninstall
|
||||
|
||||
Installs Goose v1.49 on https://127.0.0.1:${PORT} (loopback only).
|
||||
Requires: bash scripts/build-goosed-v149-local.sh (once)
|
||||
|
||||
After install, set Portal .env:
|
||||
TKMIND_API_TARGET=https://127.0.0.1:${PORT}
|
||||
EOF
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
launchctl bootout "${GUI}/${LABEL}" 2>/dev/null || true
|
||||
rm -f "${PLIST}"
|
||||
if lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
lsof -ti "TCP:${PORT}" -sTCP:LISTEN | xargs kill -TERM 2>/dev/null || true
|
||||
fi
|
||||
echo "[install-local-goose-v149] uninstalled ${LABEL}"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--uninstall" ]]; then
|
||||
uninstall
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -x "${RUN_SCRIPT}" ]]; then
|
||||
echo "run script missing: ${RUN_SCRIPT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for blocked in 18006 18007 18008 18009 18010 18011 18012 18013 18014 8081; do
|
||||
if [[ "${PORT}" == "${blocked}" ]]; then
|
||||
echo "refusing stable/production port ${PORT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "${LAUNCHD_DIR}" "${LOG_ROOT}"
|
||||
|
||||
cat > "${PLIST}" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>${RUN_SCRIPT}</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>${ROOT}</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>${LOG_ROOT}/goose-v149.launchd.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${LOG_ROOT}/goose-v149.launchd.err.log</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin</string>
|
||||
<key>GOOSE_V149_ALLOW_REAL_LLM</key>
|
||||
<string>1</string>
|
||||
<key>GOOSE_V149_PORT</key>
|
||||
<string>${PORT}</string>
|
||||
<key>GOOSE_SERVER__SECRET_KEY</key>
|
||||
<string>local-dev-secret</string>
|
||||
<key>GOOSE_V149_RUNTIME_ROOT</key>
|
||||
<string>${LOG_ROOT}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
plutil -lint "${PLIST}" >/dev/null
|
||||
launchctl bootout "${GUI}/${LABEL}" 2>/dev/null || true
|
||||
launchctl bootstrap "${GUI}" "${PLIST}"
|
||||
launchctl enable "${GUI}/${LABEL}"
|
||||
launchctl kickstart -k "${GUI}/${LABEL}"
|
||||
|
||||
echo "[install-local-goose-v149] installed ${LABEL} on https://127.0.0.1:${PORT}"
|
||||
echo "Verify: curl -sk https://127.0.0.1:${PORT}/status -H 'X-Secret-Key: local-dev-secret'"
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* WX-01..WX-13 release-gate evidence + optional live WeChat webhook smoke.
|
||||
*
|
||||
* Deterministic suites mirror release-gate/coverage.mjs:
|
||||
* - wechat-channel-contract → WX-01..08 (+ FILE-08 overlap)
|
||||
* - wechat-terminal-and-atomic → WX-09..13
|
||||
* - wechat-native-image-delivery → WX-05
|
||||
*
|
||||
* Live smoke (optional): POST /webhooks/wechat-mp/messages via run-wechat-scenario-test.mjs
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const DETERMINISTIC_SUITES = [
|
||||
{
|
||||
id: 'wechat-channel-contract',
|
||||
scenarios: ['WX-01', 'WX-02', 'WX-03', 'WX-04', 'WX-06', 'WX-07', 'WX-08'],
|
||||
command: [
|
||||
'--test',
|
||||
'wechat-mp.test.mjs',
|
||||
'wechat-oauth.test.mjs',
|
||||
'wechat-pay.test.mjs',
|
||||
'wechat-media.test.mjs',
|
||||
'wechat/wechat-channel.test.mjs',
|
||||
'wechat/image-generation-policy.test.mjs',
|
||||
'mindspace-chat-docx-package.test.mjs',
|
||||
'mindspace-html-download-links.test.mjs',
|
||||
'mindspace-public-asset-token.test.mjs',
|
||||
'server/portal-mindspace-asset-routes.test.mjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wechat-terminal-and-atomic-delivery',
|
||||
scenarios: ['WX-09', 'WX-10', 'WX-11', 'WX-12', 'WX-13'],
|
||||
command: [
|
||||
'--test',
|
||||
'user-auth.test.mjs',
|
||||
'wechat-mp.test.mjs',
|
||||
'wechat-media.test.mjs',
|
||||
'wechat/wechat-channel.test.mjs',
|
||||
'chat-image-turn-scope.test.mjs',
|
||||
'tkmind-proxy.test.mjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wechat-native-image-delivery',
|
||||
scenarios: ['WX-05'],
|
||||
command: ['--test', 'wechat-media.test.mjs'],
|
||||
},
|
||||
];
|
||||
|
||||
function runNode(args, label) {
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
const detail = result.stderr?.trim() || result.stdout?.trim() || `${label} exited ${result.status}`;
|
||||
throw new Error(`${label} failed: ${detail.slice(-2000)}`);
|
||||
}
|
||||
return result.stdout?.trim() ?? '';
|
||||
}
|
||||
|
||||
async function assertLiveEnv() {
|
||||
const { loadH5Environment } = await import('./load-env.mjs');
|
||||
loadH5Environment(import.meta.dirname);
|
||||
const missing = ['H5_WECHAT_MP_TOKEN', 'H5_WECHAT_MP_APP_ID', 'DATABASE_URL']
|
||||
.filter((key) => !String(process.env[key] ?? '').trim());
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
`live smoke 缺少环境变量: ${missing.join(', ')}。请在 .env.local 配置(见 docs/local-dev.md §公众号 Agent 调试)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
live: false,
|
||||
liveOnly: false,
|
||||
liveScenario: 'poem-page',
|
||||
liveAll: false,
|
||||
skipDeterministic: false,
|
||||
};
|
||||
for (let index = 2; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--live') options.live = true;
|
||||
else if (arg === '--live-only') {
|
||||
options.live = true;
|
||||
options.liveOnly = true;
|
||||
options.skipDeterministic = true;
|
||||
} else if (arg === '--live-all') {
|
||||
options.live = true;
|
||||
options.liveAll = true;
|
||||
} else if (arg === '--live-scenario' && argv[index + 1]) {
|
||||
options.live = true;
|
||||
options.liveScenario = argv[++index];
|
||||
} else if (arg === '--skip-deterministic') options.skipDeterministic = true;
|
||||
else if (arg === '-h' || arg === '--help') {
|
||||
console.log(`Usage:
|
||||
node scripts/run-wechat-gate-evidence.mjs
|
||||
node scripts/run-wechat-gate-evidence.mjs --live-only --live-scenario poem-page
|
||||
node scripts/run-wechat-gate-evidence.mjs --live-only --live-all
|
||||
node scripts/run-wechat-gate-evidence.mjs --skip-deterministic --live-only --live-scenario chat-general
|
||||
|
||||
Deterministic: WX-01..WX-13 unit/contract suites (no LLM).
|
||||
Live smoke: Portal :8081 + DATABASE_URL + H5_WECHAT_MP_TOKEN + H5_WECHAT_MP_APP_ID.`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv);
|
||||
const covered = new Set();
|
||||
|
||||
if (!options.skipDeterministic) {
|
||||
runNode(['--test', 'wechat-intent-router.test.mjs'], 'wechat-intent-router');
|
||||
for (const suite of DETERMINISTIC_SUITES) {
|
||||
runNode(suite.command, suite.id);
|
||||
for (const scenarioId of suite.scenarios) covered.add(scenarioId);
|
||||
console.log(`[wechat-gate] PASS ${suite.id} → ${suite.scenarios.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.live) {
|
||||
await assertLiveEnv();
|
||||
const liveArgs = ['scripts/run-wechat-scenario-test.mjs'];
|
||||
if (options.liveAll) liveArgs.push('--all');
|
||||
else liveArgs.push('--scenario', options.liveScenario);
|
||||
runNode(liveArgs, 'wechat-live-smoke');
|
||||
console.log('[wechat-gate] PASS live smoke');
|
||||
}
|
||||
|
||||
console.log('WECHAT_GATE_EVIDENCE_OK:');
|
||||
console.log(` deterministic=${options.skipDeterministic ? 'skipped' : 'WX-01..WX-13'}`);
|
||||
console.log(` live=${options.live ? (options.liveAll ? 'all-scenarios' : options.liveScenario) : 'not-run'}`);
|
||||
if (covered.size) {
|
||||
console.log(` scenarios=${[...covered].sort().join(',')}`);
|
||||
}
|
||||
if (!options.live) {
|
||||
console.log(' note=add --live-only --live-scenario poem-page for WX-04/13 webhook E2E');
|
||||
} else if (options.liveOnly) {
|
||||
console.log(' mode=live-only');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`WECHAT_GATE_EVIDENCE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -13,19 +13,9 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
|
||||
function loadDotEnv(envPath) {
|
||||
if (!fs.existsSync(envPath)) return;
|
||||
for (const line of fs.readFileSync(envPath, 'utf8').split(/\n/)) {
|
||||
if (!line || line.startsWith('#') || !line.includes('=')) continue;
|
||||
const i = line.indexOf('=');
|
||||
const key = line.slice(0, i);
|
||||
const val = line.slice(i + 1);
|
||||
if (!(key in process.env)) process.env[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
loadDotEnv(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '.env'));
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const SCENARIOS = [
|
||||
{
|
||||
@@ -48,7 +38,8 @@ const SCENARIOS = [
|
||||
id: 'poem-page',
|
||||
name: '写诗 → 做成页面',
|
||||
message: '帮我写一首关于八月午后的短诗,然后做成精美 HTML 页面',
|
||||
timeoutMs: 600_000,
|
||||
resetSessionMessage: '换新会话',
|
||||
timeoutMs: Number(process.env.WECHAT_SCENARIO_POEM_PAGE_TIMEOUT_MS ?? 900_000),
|
||||
expect: {
|
||||
forbidReply: ['没有按服务号页面技能', '没能可靠确认'],
|
||||
requireGoosedTools: ['sandbox-fs__write_file'],
|
||||
@@ -168,46 +159,81 @@ async function waitForMessageStatus(pool, { appId, openid, msgId }, timeoutMs) {
|
||||
|
||||
function grepGoosedSessionLogs(sessionId) {
|
||||
if (!sessionId) return { tools: [], sandboxFsFailed: false, lines: [], logFile: null };
|
||||
const tools = new Set();
|
||||
let sandboxFsFailed = false;
|
||||
const matchedLines = [];
|
||||
|
||||
const v149Root = process.env.GOOSE_V149_RUNTIME_ROOT || '/tmp/goose-v149-runtime';
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const v149LogDir = path.join(v149Root, 'state', 'goose', 'logs', 'cli', today);
|
||||
if (fs.existsSync(v149LogDir)) {
|
||||
for (const name of fs.readdirSync(v149LogDir)) {
|
||||
if (!name.endsWith('.log')) continue;
|
||||
const filePath = path.join(v149LogDir, name);
|
||||
let inSession = false;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const sid = obj?.fields?.session_id;
|
||||
if (sid === sessionId) inSession = true;
|
||||
if (inSession && sid && sid !== sessionId) inSession = false;
|
||||
const toolName = obj?.fields?.tool_name;
|
||||
if (inSession && toolName) {
|
||||
tools.add(toolName);
|
||||
matchedLines.push(`tool_name: ${toolName}`);
|
||||
}
|
||||
if (inSession && /Failed to load extension sandbox-fs|Transport closed/i.test(line)) {
|
||||
sandboxFsFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logGlob = `${process.env.HOME}/Library/Logs/goosed-native-*.log`;
|
||||
let loadLine = '';
|
||||
let logFile = '';
|
||||
try {
|
||||
loadLine = execSync(
|
||||
const loadLine = execSync(
|
||||
`grep -H "Session loaded.*session_id: ${sessionId}" ${logGlob} 2>/dev/null | tail -1 || true`,
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
if (loadLine.includes(':')) logFile = loadLine.split(':')[0];
|
||||
} catch {
|
||||
loadLine = '';
|
||||
logFile = '';
|
||||
}
|
||||
if (loadLine.includes(':')) {
|
||||
logFile = loadLine.split(':')[0];
|
||||
}
|
||||
let raw = '';
|
||||
if (logFile) {
|
||||
try {
|
||||
raw = execSync(
|
||||
const raw = execSync(
|
||||
`awk '/Session loaded.*session_id: ${sessionId}/{found=1; next} found && /Session loaded/{exit} found' "${logFile}" | grep "tool_name:" || true`,
|
||||
{ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 },
|
||||
);
|
||||
} catch {
|
||||
raw = '';
|
||||
}
|
||||
}
|
||||
const lines = raw.split('\n').filter(Boolean);
|
||||
const tools = [...new Set(lines.map((l) => l.match(/tool_name: ([^,]+)/)?.[1]).filter(Boolean))];
|
||||
let sandboxFsFailed = false;
|
||||
if (logFile) {
|
||||
try {
|
||||
for (const line of raw.split('\n').filter(Boolean)) {
|
||||
const toolName = line.match(/tool_name: ([^,]+)/)?.[1];
|
||||
if (toolName) {
|
||||
tools.add(toolName);
|
||||
matchedLines.push(line);
|
||||
}
|
||||
}
|
||||
const failRaw = execSync(
|
||||
`awk '/Session loaded.*session_id: ${sessionId}/{found=1} found && /Failed to load extension sandbox-fs/{print; exit}' "${logFile}" || true`,
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
sandboxFsFailed = failRaw.trim().length > 0;
|
||||
if (failRaw.trim()) sandboxFsFailed = true;
|
||||
} catch {
|
||||
sandboxFsFailed = false;
|
||||
// ignore legacy log parse errors
|
||||
}
|
||||
}
|
||||
return { tools, sandboxFsFailed, lines: lines.slice(-20), logFile: logFile || null };
|
||||
|
||||
return {
|
||||
tools: [...tools],
|
||||
sandboxFsFailed,
|
||||
lines: matchedLines.slice(-20),
|
||||
logFile: logFile || v149LogDir,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
@@ -232,6 +258,34 @@ function parseArgs(argv) {
|
||||
}
|
||||
|
||||
async function runScenario(scenario, ctx) {
|
||||
if (scenario.resetSessionMessage) {
|
||||
console.log(` 预步骤: ${scenario.resetSessionMessage}`);
|
||||
const resetMsgId = `sim_${scenario.id}_reset_${Date.now()}`;
|
||||
const resetPost = await postWechatMessage({
|
||||
baseUrl: ctx.baseUrl,
|
||||
token: ctx.token,
|
||||
appId: ctx.appId,
|
||||
openid: ctx.openid,
|
||||
ghId: ctx.ghId,
|
||||
content: scenario.resetSessionMessage,
|
||||
msgId: resetMsgId,
|
||||
});
|
||||
if (resetPost.status !== 200) {
|
||||
console.error(`✘ 预步骤 webhook 返回 ${resetPost.status}`);
|
||||
return false;
|
||||
}
|
||||
const resetOutcome = await waitForMessageStatus(
|
||||
ctx.pool,
|
||||
{ appId: ctx.appId, openid: ctx.openid, msgId: resetMsgId },
|
||||
120_000,
|
||||
);
|
||||
console.log(` 预步骤状态: ${resetOutcome.status}, session: ${resetOutcome.agent_session_id ?? '-'}`);
|
||||
if (resetOutcome.status !== 'done' && resetOutcome.status !== 'failed') {
|
||||
console.error('✘ 预步骤超时');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const msgId = `sim_${scenario.id}_${Date.now()}`;
|
||||
console.log(`\n==> 场景: ${scenario.name} (${scenario.id})`);
|
||||
console.log(` 消息: ${scenario.message.slice(0, 60)}…`);
|
||||
@@ -265,13 +319,16 @@ async function runScenario(scenario, ctx) {
|
||||
console.log(` (用户消息: ${String(outcome.displayText).slice(0, 60)}…)`);
|
||||
}
|
||||
|
||||
const requiresGoosedTools = (scenario.expect?.requireGoosedTools ?? []).length > 0;
|
||||
const goosed = grepGoosedSessionLogs(outcome.agent_session_id);
|
||||
if (goosed.sandboxFsFailed) {
|
||||
console.error('✘ goosed: sandbox-fs 扩展加载失败');
|
||||
} else if (goosed.tools.some((t) => t.startsWith('sandbox-fs__'))) {
|
||||
console.log(`✔ goosed 工具: ${goosed.tools.filter((t) => t.startsWith('sandbox-fs__')).join(', ')}`);
|
||||
} else {
|
||||
console.error(`✘ goosed 未使用 sandbox-fs 工具 (实际: ${goosed.tools.slice(0, 8).join(', ') || '无'})`);
|
||||
if (requiresGoosedTools) {
|
||||
if (goosed.sandboxFsFailed) {
|
||||
console.error('✘ goosed: sandbox-fs 扩展加载失败');
|
||||
} else if (goosed.tools.some((t) => t.startsWith('sandbox-fs__'))) {
|
||||
console.log(`✔ goosed 工具: ${goosed.tools.filter((t) => t.startsWith('sandbox-fs__')).join(', ')}`);
|
||||
} else {
|
||||
console.error(`✘ goosed 未使用 sandbox-fs 工具 (实际: ${goosed.tools.slice(0, 8).join(', ') || '无'})`);
|
||||
}
|
||||
}
|
||||
|
||||
let ok = outcome.status === 'done';
|
||||
@@ -303,7 +360,7 @@ async function runScenario(scenario, ctx) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (goosed.sandboxFsFailed) ok = false;
|
||||
if (requiresGoosedTools && goosed.sandboxFsFailed) ok = false;
|
||||
|
||||
console.log(ok ? '✔ 场景通过' : '✘ 场景失败');
|
||||
return ok;
|
||||
@@ -318,7 +375,11 @@ async function main() {
|
||||
|
||||
const token = process.env.H5_WECHAT_MP_TOKEN;
|
||||
const appId = process.env.H5_WECHAT_MP_APP_ID;
|
||||
if (!token || !appId) throw new Error('缺少 H5_WECHAT_MP_TOKEN / H5_WECHAT_MP_APP_ID');
|
||||
if (!token || !appId) {
|
||||
throw new Error(
|
||||
'缺少 H5_WECHAT_MP_TOKEN / H5_WECHAT_MP_APP_ID(写入 .env.local,见 docs/local-dev.md §公众号 Agent 调试)',
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const status = await fetch(`${baseUrl}/api/status`);
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uninstall local goosed 1.41 native launchd pool (default 18006/18007).
|
||||
set -euo pipefail
|
||||
|
||||
PORTS="${GOOSED_NATIVE_POOL_PORTS:-18006,18007}"
|
||||
LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
||||
GUI="gui/$(id -u)"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: bash scripts/uninstall-local-goosed-pool.sh [--ports 18006,18007]
|
||||
|
||||
Stops and removes per-port com.tkmind.local-goosed-* LaunchAgents.
|
||||
Does not touch 103 production or Goose v1.49 (:18049).
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ports)
|
||||
PORTS="${2:?missing value for --ports}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
IFS=',' read -r -a port_list <<< "${PORTS}"
|
||||
|
||||
for port in "${port_list[@]}"; do
|
||||
port="${port// /}"
|
||||
[[ -n "${port}" ]] || continue
|
||||
label="com.tkmind.local-goosed-${port}"
|
||||
plist="${LAUNCHD_DIR}/${label}.plist"
|
||||
launchctl bootout "${GUI}/${label}" 2>/dev/null || true
|
||||
if [[ -f "${plist}" ]]; then
|
||||
rm -f "${plist}"
|
||||
echo "removed ${plist}"
|
||||
fi
|
||||
if lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
lsof -ti "TCP:${port}" -sTCP:LISTEN | xargs kill -TERM 2>/dev/null || true
|
||||
sleep 1
|
||||
lsof -ti "TCP:${port}" -sTCP:LISTEN | xargs kill -9 2>/dev/null || true
|
||||
echo "stopped listener on port ${port}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Legacy single-instance label (pre per-port pool).
|
||||
launchctl bootout "${GUI}/com.tkmind.local-goosed" 2>/dev/null || true
|
||||
legacy_plist="${LAUNCHD_DIR}/com.tkmind.local-goosed.plist"
|
||||
if [[ -f "${legacy_plist}" ]]; then
|
||||
rm -f "${legacy_plist}"
|
||||
echo "removed legacy plist: ${legacy_plist}"
|
||||
fi
|
||||
|
||||
echo "[uninstall-local-goosed-pool] done; ports=${PORTS}"
|
||||
Reference in New Issue
Block a user