fix(mindspace): restore publish guards and safe 103 goosed canary routing
Finish-sync guards now accept SEO/GEO disk enrichment instead of requiring byte-identical HTML, and fragment writes no longer duplicate doctype. Users canary mode keeps the full native pool while routing allowlisted identities to :18015 without overriding memory settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,3 +48,21 @@ ssh john@114.85.107.50 'shasum -a 256 /Users/john/Project/tkmind_go-native/relea
|
||||
3. 新 goosed 二进制(从干净 commit 构建)按 cutover runbook 替换,旧二进制保留在 `releases/`。
|
||||
|
||||
每一步都需单独批准。
|
||||
|
||||
## 2026-09-23 后续执行记录
|
||||
|
||||
| 时间 (UTC+8) | 动作 | 结果 |
|
||||
|---|---|---|
|
||||
| 17:34 | Portal 快速发布 `20260923-172809-5e2f27d6`(`main` @ `5e2f27d6`) | 8081/公网验证通过;测试页 `a2fcf61f-6387-49fb-a52b-e3bfdb79131e` |
|
||||
| 17:34+ | goosed 全池加固 + 滚动重启 ×2 | 9 路 `GOOSED_NOFILE=65536`;二进制 `goose-20260923-v149-m4-508df26f5` |
|
||||
| 17:34+ | `CODEX_HARNESS_MYSQL` → Portal `scripts/goosed-harness-mysql2-cli.mjs` | harness remember 应恢复(RDS `SELECT 1` 已通过) |
|
||||
| — | 9 实例池规模 | **维持 9 路**(用户决定暂不缩减) |
|
||||
| — | `:18015` 独立金丝雀 | `GOOSE_MAX_TURNS=100`、`GOOSE_MAX_TOOL_REPETITIONS=5`(未扩到全池) |
|
||||
|
||||
备份:
|
||||
- `memind-full-20260923-172809-5e2f27d6-before.tar.gz`
|
||||
- `memind-persisted-20260923-172809-5e2f27d6-before.tar.gz`
|
||||
- `archived_source=Memind-source-before-20260923-172809-5e2f27d6`
|
||||
- goosed:`backups/pre-pool-m4-*`、`backups/pre-hardening-*`
|
||||
|
||||
回滚 goosed:`GOOSE_BIN=releases/goose-20260923-v149-m3-4aea5abf` + `goose-v149-pool-hardening-103.sh --rollback`(或更早 backup)。
|
||||
|
||||
@@ -35,7 +35,8 @@ export function upsertMindspaceCoverMeta(html, coverMeta, { replace = false } =
|
||||
if (/<html[^>]*>/i.test(source)) {
|
||||
return source.replace(/<html([^>]*)>/i, `<html$1><head>${metaTag}</head>`);
|
||||
}
|
||||
return `<!doctype html><html lang="zh-CN"><head>${metaTag}</head><body>${source}</body></html>`;
|
||||
const body = source.replace(/^\s*<!doctype[^>]*>\s*/i, '');
|
||||
return `<!doctype html><html lang="zh-CN"><head>${metaTag}</head><body>${body}</body></html>`;
|
||||
}
|
||||
|
||||
export function normalizeCoverMetaSuggestion(raw) {
|
||||
|
||||
@@ -19,3 +19,11 @@ test('upsertMindspaceCoverMeta inserts meta into head when missing', () => {
|
||||
assert.match(next, /name="mindspace-cover"/);
|
||||
assert.equal(parseMindspaceCoverMeta(next).tag, '美食');
|
||||
});
|
||||
|
||||
test('upsertMindspaceCoverMeta wraps fragments without duplicating doctype', () => {
|
||||
const html = '<!doctype html><title>Guarded release</title>';
|
||||
const next = upsertMindspaceCoverMeta(html, { tag: '精选页面', subtitle: 'Guarded release' });
|
||||
assert.match(next, /^<!doctype html>/i);
|
||||
assert.doesNotMatch(next, /<body>\s*<!doctype html>/i);
|
||||
assert.match(next, /<title>Guarded release<\/title>/);
|
||||
});
|
||||
|
||||
@@ -186,8 +186,9 @@ test('materializeMissingPublicHtmlWrites writes html from sandbox write_file too
|
||||
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
|
||||
assert.deepEqual(result.materialized, ['public/summer-essay.html']);
|
||||
const saved = fs.readFileSync(path.join(publishDir, 'public/summer-essay.html'), 'utf8');
|
||||
assert.match(saved, /<!doctype html><title>Summer<\/title>/i);
|
||||
assert.match(saved, /<title>Summer<\/title>/i);
|
||||
assert.match(saved, /name="mindspace-geo"/);
|
||||
assert.doesNotMatch(saved, /<body>\s*<!doctype html>/i);
|
||||
} finally {
|
||||
fs.rmSync(publishDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -420,8 +421,9 @@ test('materializeMissingPublicHtmlWrites writes html from developer write tool c
|
||||
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
|
||||
assert.deepEqual(result.materialized, ['public/guizhou-guide.html']);
|
||||
const saved = fs.readFileSync(path.join(publishDir, 'public/guizhou-guide.html'), 'utf8');
|
||||
assert.match(saved, /<!doctype html><title>Guizhou<\/title>/i);
|
||||
assert.match(saved, /<title>Guizhou<\/title>/i);
|
||||
assert.match(saved, /name="mindspace-geo"/);
|
||||
assert.doesNotMatch(saved, /<body>\s*<!doctype html>/i);
|
||||
} finally {
|
||||
fs.rmSync(publishDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -99,6 +99,26 @@ const BLOCKED_HOSTS = new Set([
|
||||
export const DEFAULT_GOOSE_STABLE_TARGET = 'https://127.0.0.1:18006';
|
||||
export const DEFAULT_GOOSE_V149_TARGET = 'https://127.0.0.1:18049';
|
||||
|
||||
export function parseConfiguredPoolTargets(env = process.env) {
|
||||
const csvTargets = String(env.TKMIND_API_TARGETS ?? '')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const legacyTargets = [
|
||||
env.TKMIND_API_TARGET ?? DEFAULT_GOOSE_STABLE_TARGET,
|
||||
env.TKMIND_API_TARGET_1,
|
||||
].filter(Boolean);
|
||||
const seen = new Set();
|
||||
const targets = [];
|
||||
for (const raw of [...csvTargets, ...legacyTargets]) {
|
||||
const normalized = assertLoopbackGooseTarget(raw, 'pool goosed target');
|
||||
if (seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
targets.push(normalized);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
function csvSet(value) {
|
||||
return new Set(
|
||||
String(value ?? '')
|
||||
@@ -159,10 +179,12 @@ export function resolveGooseApiTargetsFromEnv(env = process.env) {
|
||||
const mode = parseGooseCanaryMode(env);
|
||||
if (mode === 'off') return null;
|
||||
|
||||
const stable = assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET_STABLE ?? DEFAULT_GOOSE_STABLE_TARGET,
|
||||
'TKMIND_API_TARGET_STABLE',
|
||||
);
|
||||
const poolTargets = parseConfiguredPoolTargets(env);
|
||||
const stable = poolTargets[0]
|
||||
?? assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET_STABLE ?? DEFAULT_GOOSE_STABLE_TARGET,
|
||||
'TKMIND_API_TARGET_STABLE',
|
||||
);
|
||||
const v149 = assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET_V149 ?? DEFAULT_GOOSE_V149_TARGET,
|
||||
'TKMIND_API_TARGET_V149',
|
||||
@@ -186,12 +208,14 @@ export function resolveGooseApiTargetsFromEnv(env = process.env) {
|
||||
);
|
||||
}
|
||||
|
||||
const targets = [...new Set([...poolTargets, v149])];
|
||||
|
||||
return Object.freeze({
|
||||
mode,
|
||||
stable,
|
||||
v149,
|
||||
primary: stable,
|
||||
targets: [stable, v149],
|
||||
targets,
|
||||
policy,
|
||||
});
|
||||
}
|
||||
@@ -201,8 +225,11 @@ export function resolveGooseTargetForIdentity(identity, config) {
|
||||
return config?.stable ?? DEFAULT_GOOSE_STABLE_TARGET;
|
||||
}
|
||||
if (config.mode === 'all') return config.v149;
|
||||
const targetKey = resolveCanaryTarget(identity, config.policy);
|
||||
return targetKey === 'candidate' ? config.v149 : config.stable;
|
||||
if (config.mode === 'users') {
|
||||
const targetKey = resolveCanaryTarget(identity, config.policy);
|
||||
return targetKey === 'candidate' ? config.v149 : null;
|
||||
}
|
||||
return config.stable;
|
||||
}
|
||||
|
||||
export function describeGooseCanaryConfig(env = process.env) {
|
||||
|
||||
@@ -25,29 +25,34 @@ test('resolveGooseApiTargetsFromEnv all mode pins Portal to v149', () => {
|
||||
assert.deepEqual(config.targets, ['https://127.0.0.1:18049']);
|
||||
});
|
||||
|
||||
test('resolveGooseApiTargetsFromEnv users mode exposes dual targets', () => {
|
||||
test('resolveGooseApiTargetsFromEnv users mode keeps full pool and adds v149', () => {
|
||||
const config = resolveGooseApiTargetsFromEnv({
|
||||
TKMIND_GOOSE_CANARY: 'users',
|
||||
TKMIND_GOOSE_CANARY_USER_IDS: 'user-1',
|
||||
TKMIND_API_TARGETS: 'https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008',
|
||||
TKMIND_API_TARGET_V149: 'https://127.0.0.1:18015',
|
||||
});
|
||||
assert.equal(config.mode, 'users');
|
||||
assert.equal(config.primary, 'https://127.0.0.1:18006');
|
||||
assert.deepEqual(config.targets, ['https://127.0.0.1:18006', 'https://127.0.0.1:18049']);
|
||||
assert.deepEqual(config.targets, [
|
||||
'https://127.0.0.1:18006',
|
||||
'https://127.0.0.1:18007',
|
||||
'https://127.0.0.1:18008',
|
||||
'https://127.0.0.1:18015',
|
||||
]);
|
||||
});
|
||||
|
||||
test('resolveGooseTargetForIdentity routes canary users to v149', () => {
|
||||
test('resolveGooseTargetForIdentity routes canary users to v149 only', () => {
|
||||
const config = resolveGooseApiTargetsFromEnv({
|
||||
TKMIND_GOOSE_CANARY: 'users',
|
||||
TKMIND_GOOSE_CANARY_USER_IDS: 'user-abc',
|
||||
TKMIND_API_TARGET_V149: 'https://127.0.0.1:18015',
|
||||
});
|
||||
assert.equal(
|
||||
resolveGooseTargetForIdentity({ id: 'user-abc' }, config),
|
||||
'https://127.0.0.1:18049',
|
||||
);
|
||||
assert.equal(
|
||||
resolveGooseTargetForIdentity({ id: 'other-user' }, config),
|
||||
'https://127.0.0.1:18006',
|
||||
'https://127.0.0.1:18015',
|
||||
);
|
||||
assert.equal(resolveGooseTargetForIdentity({ id: 'other-user' }, config), null);
|
||||
});
|
||||
|
||||
test('assertLoopbackGooseTarget refuses production hosts', () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Toggle Goose v1.49 canary routing on 103 stable Portal (.env).
|
||||
# users mode keeps the full native pool in TKMIND_API_TARGETS and only routes
|
||||
# allowlisted identities to TKMIND_API_TARGET_V149 (:18015 by default).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ENV_FILE="${GOOSE_V149_CANARY_ENV_FILE:-/Users/john/Project/Memind/.env}"
|
||||
MARKER_BEGIN='# GOOSE_V149_CANARY_BEGIN'
|
||||
MARKER_END='# GOOSE_V149_CANARY_END'
|
||||
STABLE_TARGET="${TKMIND_API_TARGET_STABLE:-https://127.0.0.1:18006}"
|
||||
V149_TARGET="${TKMIND_API_TARGET_V149:-https://127.0.0.1:18015}"
|
||||
PORTAL_LABEL="${MEMIND_PORTAL_LABEL:-cn.tkmind.memind-portal}"
|
||||
GUI="gui/$(id -u)"
|
||||
@@ -16,12 +17,16 @@ usage() {
|
||||
Usage: bash scripts/switch-goose-v149-canary-103.sh <command>
|
||||
|
||||
Commands:
|
||||
on [all|users] Route Portal to v1.49 canary (${V149_TARGET})
|
||||
on [all|users] Route allowlisted users (or everyone in all mode) to v1.49 canary (${V149_TARGET})
|
||||
off|rollback Remove canary block from ${ENV_FILE}
|
||||
status Show active block
|
||||
|
||||
users mode example (唐 only):
|
||||
TKMIND_GOOSE_CANARY_WECHAT_USER_IDS=wx_ul610et8 bash scripts/switch-goose-v149-canary-103.sh on users
|
||||
users mode example (john only):
|
||||
TKMIND_GOOSE_CANARY_USERNAMES=john bash scripts/switch-goose-v149-canary-103.sh on users
|
||||
|
||||
Notes:
|
||||
- Does not rewrite TKMIND_API_TARGETS or memory settings.
|
||||
- all mode is intended for local loopback canary only; avoid on the 9-instance 103 pool.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -45,22 +50,14 @@ write_canary_block() {
|
||||
{
|
||||
echo "${MARKER_BEGIN}"
|
||||
echo "TKMIND_GOOSE_CANARY=${mode}"
|
||||
echo "TKMIND_API_TARGET_STABLE=${STABLE_TARGET}"
|
||||
echo "TKMIND_API_TARGET_V149=${V149_TARGET}"
|
||||
if [[ "${mode}" == "all" ]]; then
|
||||
echo "TKMIND_API_TARGET=${V149_TARGET}"
|
||||
elif [[ "${mode}" == "users" ]]; then
|
||||
echo "TKMIND_API_TARGET=${STABLE_TARGET}"
|
||||
echo "TKMIND_API_TARGETS=${STABLE_TARGET},${V149_TARGET}"
|
||||
[[ -n "${TKMIND_GOOSE_CANARY_USER_IDS:-}" ]] && echo "TKMIND_GOOSE_CANARY_USER_IDS=${TKMIND_GOOSE_CANARY_USER_IDS}"
|
||||
[[ -n "${TKMIND_GOOSE_CANARY_USERNAMES:-}" ]] && echo "TKMIND_GOOSE_CANARY_USERNAMES=${TKMIND_GOOSE_CANARY_USERNAMES}"
|
||||
[[ -n "${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS:-}" ]] && echo "TKMIND_GOOSE_CANARY_WECHAT_USER_IDS=${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS}"
|
||||
fi
|
||||
echo "MEMORY_BACKEND=legacy"
|
||||
echo "MEMORY_VECTOR_ENABLED=0"
|
||||
echo "MEMORY_AGENT_INJECTION_MODE=off"
|
||||
echo "MEMORY_LIFECYCLE_ENABLED=0"
|
||||
echo "MEMORY_CANDIDATE_ENABLED=0"
|
||||
echo "${MARKER_END}"
|
||||
} >> "${ENV_FILE}"
|
||||
}
|
||||
@@ -79,6 +76,9 @@ case "${cmd}" in
|
||||
echo "Missing ${ENV_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${mode}" == "all" ]]; then
|
||||
echo "[goose-v149-canary-103] WARN: all mode replaces the entire pool with v149; use users mode on 103 production" >&2
|
||||
fi
|
||||
if [[ "${mode}" == "users" && -z "${TKMIND_GOOSE_CANARY_USER_IDS:-}${TKMIND_GOOSE_CANARY_USERNAMES:-}${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS:-}" ]]; then
|
||||
echo "users mode requires identity allowlist env vars" >&2
|
||||
exit 2
|
||||
|
||||
@@ -12,6 +12,10 @@ const root = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const runtimeServer = path.join(root, '.runtime', 'portal', 'server.mjs');
|
||||
const currentUser = { id: 'a6fb1e97-2b0f-447b-b138-4561d8e5c53e', username: 'john' };
|
||||
|
||||
const RAW_HTML =
|
||||
'<!doctype html><html lang="zh-CN"><head><title>Guarded release</title></head>'
|
||||
+ '<body><h1>Guarded release</h1></body></html>';
|
||||
|
||||
const toolRequestOnlyMessages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
@@ -23,7 +27,7 @@ const toolRequestOnlyMessages = [
|
||||
name: 'write',
|
||||
arguments: {
|
||||
path: 'public/guarded-release.html',
|
||||
content: '<!doctype html><title>Guarded release</title>',
|
||||
content: RAW_HTML,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -49,10 +53,11 @@ try {
|
||||
publishDir,
|
||||
});
|
||||
assert.deepEqual(result.materialized, ['public/guarded-release.html']);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(publishDir, 'public/guarded-release.html'), 'utf8'),
|
||||
'<!doctype html><title>Guarded release</title>',
|
||||
);
|
||||
const written = fs.readFileSync(path.join(publishDir, 'public/guarded-release.html'), 'utf8');
|
||||
assert.ok(written.includes('<title>Guarded release</title>'));
|
||||
assert.ok(written.includes('<h1>Guarded release</h1>'));
|
||||
assert.ok(/data-mindspace-seo-outline=|name="mindspace-geo"/i.test(written));
|
||||
assert.doesNotMatch(written, /<body>\s*<!doctype html>/i);
|
||||
} finally {
|
||||
fs.rmSync(publishDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
+1
-3
@@ -1395,9 +1395,7 @@ export function createTkmindProxy({
|
||||
},
|
||||
gooseCanaryConfig,
|
||||
);
|
||||
if (target && (targets.includes(target) || target === gooseCanaryConfig.v149 || target === gooseCanaryConfig.stable)) {
|
||||
if (await targetHealthy(target)) return target;
|
||||
}
|
||||
if (target && await targetHealthy(target)) return target;
|
||||
}
|
||||
return pickTarget();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user