Add Rain V0 for MeInput full-range chat analysis and delivery tooling.
Memind CI / Test, build, and release guards (push) Has been cancelled
Memind CI / Test, build, and release guards (push) Has been cancelled
Introduce rain-service orchestration, browser-safe chat skill filtering, MeInput adapter helpers, and verify/deploy scripts so Rain mode can summarize recent input without Memory V2 pollution. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -205,6 +205,7 @@ async function bootstrapWorker() {
|
||||
tkmindProxy,
|
||||
toolGateway,
|
||||
directChatService,
|
||||
llmProviderService,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
chatIntentRouter,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Deploy MeInput tutorial + case pages to 103 production under 唐 user.
|
||||
* Usage: node scripts/deploy-tang-meinput-pages-103.mjs [--send-wechat]
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const HOST = 'john@58.38.22.103';
|
||||
const REMOTE_ROOT = '/Users/john/Project/Memind';
|
||||
const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db';
|
||||
const JOHN_LOCAL = '1c99b83b-0454-474f-a5d2-129d34506a32';
|
||||
const FILES = [
|
||||
'meinput-tkmind-portrait-tutorial.html',
|
||||
'meinput-tkmind-portrait-tutorial-wechat.html',
|
||||
'behavior-pattern-analysis.html',
|
||||
];
|
||||
const PUBLIC_BASE = `https://m.tkmind.cn/MindSpace/${TANG}/public`;
|
||||
|
||||
const NODE103 = '/opt/homebrew/opt/node@24/bin/node';
|
||||
const sendWechat = process.argv.includes('--send-wechat');
|
||||
|
||||
function sh(cmd) {
|
||||
execSync(cmd, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
const remoteScript = `
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
const TANG = '${TANG}';
|
||||
const REMOTE_ROOT = '${REMOTE_ROOT}';
|
||||
const FILES = ${JSON.stringify(FILES)};
|
||||
const REQUEST_ID = 'deploy-meinput-tutorial-20260904';
|
||||
const PUBLIC_BASE = '${PUBLIC_BASE}';
|
||||
|
||||
process.loadEnvFile(path.join(REMOTE_ROOT, '.env'));
|
||||
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
|
||||
|
||||
async function ensureReady(relativePath) {
|
||||
const now = Date.now();
|
||||
const id = crypto.randomUUID();
|
||||
await pool.query(
|
||||
\`INSERT INTO h5_page_delivery_contracts
|
||||
(id, user_id, request_id, workspace_relative_path, data_mode, status, ready_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'static', 'ready', ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE status = 'ready', ready_at = VALUES(ready_at), failure_reason = NULL, updated_at = VALUES(updated_at)\`,
|
||||
[id, TANG, REQUEST_ID, relativePath, now, now, now],
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const results = [];
|
||||
for (const name of FILES) {
|
||||
const relativePath = 'public/' + name;
|
||||
const abs = path.join(REMOTE_ROOT, 'MindSpace', TANG, relativePath);
|
||||
const exists = fs.existsSync(abs);
|
||||
const size = exists ? fs.statSync(abs).size : 0;
|
||||
if (exists) await ensureReady(relativePath);
|
||||
results.push({ file: name, exists, size, url: PUBLIC_BASE + '/' + name });
|
||||
}
|
||||
console.log(JSON.stringify({ ok: true, pages: results }, null, 2));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
`.trim();
|
||||
|
||||
async function sendWechatLinks() {
|
||||
const remoteWechat = `
|
||||
import path from 'node:path';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
const TANG = '${TANG}';
|
||||
const PUBLIC_BASE = '${PUBLIC_BASE}';
|
||||
const FILES = ${JSON.stringify(FILES)};
|
||||
|
||||
process.loadEnvFile('${REMOTE_ROOT}/.env');
|
||||
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
|
||||
const appId = process.env.H5_WECHAT_MP_APP_ID ?? process.env.WECHAT_MP_APP_ID;
|
||||
const appSecret = process.env.H5_WECHAT_MP_APP_SECRET ?? process.env.WECHAT_MP_APP_SECRET;
|
||||
if (!appId || !appSecret) throw new Error('missing wechat credentials');
|
||||
|
||||
const [ident] = await pool.query(
|
||||
'SELECT openid FROM h5_user_wechat_identities WHERE user_id = ? AND app_id = ? LIMIT 1',
|
||||
[TANG, appId],
|
||||
);
|
||||
const openid = ident?.[0]?.openid;
|
||||
if (!openid) throw new Error('tang not bound to wechat');
|
||||
|
||||
const tokenRes = await fetch('https://api.weixin.qq.com/cgi-bin/stable_token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ grant_type: 'client_credential', appid: appId, secret: appSecret }),
|
||||
});
|
||||
const tokenPayload = await tokenRes.json();
|
||||
if (!tokenPayload.access_token) throw new Error(JSON.stringify(tokenPayload));
|
||||
|
||||
const lines = [
|
||||
'MeInput × TKMind 教程与案例已发布:',
|
||||
'',
|
||||
'📱 教程(公众号发布版)',
|
||||
PUBLIC_BASE + '/meinput-tkmind-portrait-tutorial-wechat.html',
|
||||
'',
|
||||
'📖 教程(网页阅读版)',
|
||||
PUBLIC_BASE + '/meinput-tkmind-portrait-tutorial.html',
|
||||
'',
|
||||
'🧭 案例:用户A 全景画像',
|
||||
PUBLIC_BASE + '/behavior-pattern-analysis.html',
|
||||
];
|
||||
const text = lines.join('\\n').slice(0, 2048);
|
||||
|
||||
const sendRes = await fetch('https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=' + encodeURIComponent(tokenPayload.access_token), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ touser: openid, msgtype: 'text', text: { content: text } }),
|
||||
});
|
||||
const sendPayload = await sendRes.json();
|
||||
if (Number(sendPayload.errcode ?? 0) !== 0) throw new Error(JSON.stringify(sendPayload));
|
||||
console.log(JSON.stringify({ ok: true, sent: true, openid: openid.slice(0, 8) + '...' }));
|
||||
await pool.end();
|
||||
`.trim();
|
||||
|
||||
const tmp = `${REMOTE_ROOT}/.tmp-tang-meinput-wechat.mjs`;
|
||||
fs.writeFileSync(path.join(root, '.tmp-tang-wechat.mjs'), remoteWechat);
|
||||
sh(`scp -q ${path.join(root, '.tmp-tang-wechat.mjs')} ${HOST}:${tmp}`);
|
||||
sh(`ssh -o BatchMode=yes ${HOST} 'cd ${REMOTE_ROOT} && ${NODE103} ${tmp} && rm -f ${tmp}'`);
|
||||
fs.unlinkSync(path.join(root, '.tmp-tang-wechat.mjs'));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const localDir = path.join(root, 'MindSpace', JOHN_LOCAL, 'public');
|
||||
for (const f of FILES) {
|
||||
const src = path.join(localDir, f);
|
||||
if (!fs.existsSync(src)) throw new Error(`missing local file: ${src}`);
|
||||
}
|
||||
|
||||
const remotePublic = `${REMOTE_ROOT}/MindSpace/${TANG}/public`;
|
||||
sh(`ssh -o BatchMode=yes ${HOST} 'mkdir -p ${remotePublic}'`);
|
||||
for (const f of FILES) {
|
||||
sh(`scp -q ${path.join(localDir, f)} ${HOST}:${remotePublic}/${f}`);
|
||||
}
|
||||
|
||||
const tmpLocal = path.join(root, '.tmp-tang-deploy-103.mjs');
|
||||
fs.writeFileSync(tmpLocal, remoteScript);
|
||||
const tmpRemote = `${REMOTE_ROOT}/.tmp-tang-meinput-deploy.mjs`;
|
||||
sh(`scp -q ${tmpLocal} ${HOST}:${tmpRemote}`);
|
||||
sh(`ssh -o BatchMode=yes ${HOST} 'cd ${REMOTE_ROOT} && ${NODE103} ${tmpRemote} && rm -f ${tmpRemote}'`);
|
||||
fs.unlinkSync(tmpLocal);
|
||||
|
||||
console.log('\n=== Production URLs ===');
|
||||
for (const f of FILES) console.log(`${PUBLIC_BASE}/${f}`);
|
||||
|
||||
for (const f of FILES) {
|
||||
const code = execSync(
|
||||
`curl -sS -o /dev/null -w '%{http_code}' 'https://m.tkmind.cn/MindSpace/${TANG}/public/${f}'`,
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
console.log(`${f}: HTTP ${code}`);
|
||||
}
|
||||
|
||||
if (sendWechat) {
|
||||
await sendWechatLinks();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 用户A · MeInput 全景用户画像与行为节律报告(非工程日志)
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { markPageDeliveryContractReady } from '../mindspace-delivery-contract.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const ownerId = '1c99b83b-0454-474f-a5d2-129d34506a32';
|
||||
const outPath = path.join(root, 'MindSpace', ownerId, 'public', 'behavior-pattern-analysis.html');
|
||||
|
||||
const MEINPUT_USER = '3f24dcbb-0505-4f0a-a432-828f608e2448';
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function fmtCst(iso) {
|
||||
return new Date(iso).toLocaleString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
function segmentRows(rows, gapMs = 2000) {
|
||||
const out = [];
|
||||
let cur = null;
|
||||
for (const r of rows) {
|
||||
const ms = new Date(r.created_at).getTime();
|
||||
if (!cur || ms - cur.last > gapMs) {
|
||||
if (cur) out.push(cur);
|
||||
cur = { start: r.created_at, end: r.created_at, last: ms, parts: [], app: r.app_name || r.app_bundle_id };
|
||||
}
|
||||
cur.parts.push(r.text);
|
||||
cur.end = r.created_at;
|
||||
cur.last = ms;
|
||||
}
|
||||
if (cur) out.push(cur);
|
||||
return out.map((s) => ({ start: s.start, end: s.end, text: s.parts.join(''), app: s.app }));
|
||||
}
|
||||
|
||||
function appLabel(app) {
|
||||
const s = String(app || '');
|
||||
if (s.includes('todesktop') || s.includes('Cursor')) return '深度开发';
|
||||
if (s.includes('WeWork') || s.includes('WeChat')) return '协作沟通';
|
||||
return '其他场景';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.loadEnvFile?.(path.join(root, '.env'));
|
||||
const mePool = mysql.createPool({ uri: process.env.MEINPUT_DATABASE_URL, connectionLimit: 2 });
|
||||
const [rows] = await mePool.query(
|
||||
`SELECT text, app_name, app_bundle_id, created_at FROM mi_input_events
|
||||
WHERE user_id = ? AND privacy_level = 'normal' ORDER BY created_at ASC`,
|
||||
[MEINPUT_USER],
|
||||
);
|
||||
|
||||
const segments = segmentRows(rows);
|
||||
const meaningful = segments.filter((s) => s.text.replace(/\s/g, '').length >= 3);
|
||||
|
||||
const buckets15 = {};
|
||||
for (const r of rows) {
|
||||
const cst = new Date(r.created_at.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
|
||||
const h = cst.getHours();
|
||||
const m = Math.floor(cst.getMinutes() / 15) * 15;
|
||||
const key = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
buckets15[key] = (buckets15[key] || 0) + 1;
|
||||
}
|
||||
const peak = Object.entries(buckets15).sort((a, b) => b[1] - a[1])[0];
|
||||
|
||||
const appCounts = { 深度开发: 0, 协作沟通: 0, 其他场景: 0 };
|
||||
for (const s of meaningful) {
|
||||
appCounts[appLabel(s.app)] = (appCounts[appLabel(s.app)] || 0) + 1;
|
||||
}
|
||||
const appTotal = meaningful.length || 1;
|
||||
const devPct = Math.round((appCounts['深度开发'] / appTotal) * 100);
|
||||
const collabPct = Math.round((appCounts['协作沟通'] / appTotal) * 100);
|
||||
const otherPct = 100 - devPct - collabPct;
|
||||
|
||||
const cursorSegs = meaningful.filter((s) => appLabel(s.app) === '深度开发');
|
||||
const wecomSegs = meaningful.filter((s) => appLabel(s.app) === '协作沟通');
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>用户A · 全景用户画像与行为节律</title>
|
||||
<meta name="description" content="基于 MeInput 输入还原的全景用户画像:角色、事项、偏好、节律、协作、风险与效率建议">
|
||||
<meta name="mindspace-cover" content='{"tag":"画像","emoji":"🧭","accent":"#5c4d7d","accent2":"#2d1b4e","subtitle":"全景画像 · 十维洞察 · 效率建议"}'>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1419; --surface: #1a2332; --card: #243044; --text: #e7ecf3;
|
||||
--muted: #8fa3bf; --accent: #c4b5fd; --accent2: #8b5cf6; --warm: #fbbf24; --ok: #6ee7b7; --warn: #fca5a5;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg); color: var(--text); line-height: 1.7;
|
||||
}
|
||||
.hero {
|
||||
background: linear-gradient(145deg, #2d1b4e 0%, #5c4d7d 45%, #1b263b 100%);
|
||||
padding: 52px 20px 44px; text-align: center;
|
||||
}
|
||||
.hero h1 { font-size: 1.9rem; margin-bottom: 10px; }
|
||||
.hero .sub { color: rgba(255,255,255,.88); max-width: 680px; margin: 0 auto; font-size: 1.05rem; }
|
||||
.hero .tag { display: inline-block; margin-top: 18px; padding: 6px 16px; border-radius: 999px; background: rgba(255,255,255,.12); font-size: .82rem; }
|
||||
.container { max-width: 820px; margin: 0 auto; padding: 36px 18px 72px; }
|
||||
h2 {
|
||||
font-size: 1.2rem; color: var(--accent); margin: 36px 0 16px;
|
||||
border-left: 4px solid var(--accent2); padding-left: 12px;
|
||||
}
|
||||
.card {
|
||||
background: var(--surface); border: 1px solid #2a3a50; border-radius: 14px;
|
||||
padding: 22px 24px; margin-bottom: 16px;
|
||||
}
|
||||
.card h3 { font-size: 1rem; color: var(--warm); margin-bottom: 8px; }
|
||||
.card p, .card li { color: #c9d7ea; font-size: .95rem; }
|
||||
.card ul { padding-left: 1.15rem; }
|
||||
.card li { margin-bottom: 8px; }
|
||||
.profile-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 8px; }
|
||||
.pill {
|
||||
background: var(--card); border-radius: 10px; padding: 14px; text-align: center;
|
||||
}
|
||||
.pill b { display: block; font-size: 1.1rem; color: var(--ok); }
|
||||
.pill span { font-size: .75rem; color: var(--muted); }
|
||||
.heat { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
.heat span {
|
||||
padding: 8px 12px; border-radius: 8px; font-size: .82rem;
|
||||
background: var(--card); border: 1px solid #334155;
|
||||
}
|
||||
.heat .hot { background: #3b2f5c; border-color: var(--accent2); color: var(--accent); font-weight: 600; }
|
||||
.quote {
|
||||
border-left: 3px solid var(--accent2); padding: 10px 14px; margin: 12px 0;
|
||||
background: #152033; color: var(--muted); font-size: .88rem; font-style: italic;
|
||||
}
|
||||
.priority { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 14px; }
|
||||
.priority .rank {
|
||||
flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%;
|
||||
background: var(--accent2); color: #fff; display: flex; align-items: center; justify-content: center;
|
||||
font-size: .82rem; font-weight: 700;
|
||||
}
|
||||
.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: .88rem; }
|
||||
.bar-row .lbl { width: 72px; color: var(--muted); flex-shrink: 0; }
|
||||
.bar-row .track { flex: 1; height: 8px; background: #1e293b; border-radius: 4px; overflow: hidden; }
|
||||
.bar-row .fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--accent2), var(--ok)); }
|
||||
.bar-row .pct { width: 36px; text-align: right; color: var(--ok); font-weight: 600; }
|
||||
.suggest {
|
||||
background: #152033; border: 1px solid #334155; border-radius: 10px;
|
||||
padding: 14px 16px; margin-bottom: 10px;
|
||||
}
|
||||
.suggest b { color: var(--warm); display: block; margin-bottom: 6px; font-size: .92rem; }
|
||||
.suggest p { margin: 0; font-size: .88rem; color: #b8c9de; }
|
||||
.two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }
|
||||
footer { text-align: center; color: var(--muted); font-size: .75rem; margin-top: 40px; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: var(--accent); text-decoration: none; font-size: .88rem; }
|
||||
.back:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body data-mindspace-page-tag="platform-brand">
|
||||
<header class="hero">
|
||||
<h1>用户A · 全景用户画像与行为节律</h1>
|
||||
<p class="sub">十维洞察:角色、事项、偏好、决策、节律、场景、协作、行为模式、风险与效率建议——帮你看清「是谁、忙什么、何时最高效、下一步该做什么」。</p>
|
||||
<span class="tag">观测窗口:近期输入 · ${rows.length} 条事件 · ${meaningful.length} 段有效语义 · 已脱敏</span>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<a class="back" href="meinput-tkmind-portrait-tutorial.html">← 返回教程文章</a>
|
||||
|
||||
<section class="profile-grid">
|
||||
<div class="pill"><b>产品负责人</b><span>亲自验收 · 追问到底</span></div>
|
||||
<div class="pill"><b>AI 原生取向</b><span>智能体验优先</span></div>
|
||||
<div class="pill"><b>移动端优先</b><span>真机 / 安装包验收</span></div>
|
||||
<div class="pill"><b>夜间深专注</b><span>05:15–06:15 峰值</span></div>
|
||||
<div class="pill"><b>确认型决策</b><span>要眼见为实</span></div>
|
||||
<div class="pill"><b>双通道协作</b><span>开发 + IM 并行</span></div>
|
||||
</section>
|
||||
|
||||
<h2>一、角色定位(这个人是谁)</h2>
|
||||
<div class="card">
|
||||
<p><strong>用户A</strong>是典型的<strong>产品型创始人 / 负责人</strong>:不只定方向,还亲自安装包、测登录、对后台数据核对到「有没有进库」。沟通短、指令清晰,遇到阻塞会直接追问「卡在哪了?」。</p>
|
||||
<p style="margin-top:12px">工作形态呈现<strong>「Owner + 验收者」</strong>双重角色:自己上手试一遍,同时指挥同事去后台核对——既不脱离细节,也不单打独斗。</p>
|
||||
<p style="margin-top:12px">输入内容高度聚焦<strong>产品能否上线、链路是否打通</strong>,极少闲聊或无关话题,说明当前处于<strong>交付攻坚期</strong>而非探索期。</p>
|
||||
</div>
|
||||
|
||||
<h2>二、近期重要事项(按优先级)</h2>
|
||||
<div class="card">
|
||||
<div class="priority"><span class="rank">1</span><div><strong>登录与后台数据闭环</strong> · <span style="color:var(--warn)">未闭环</span><br>反复出现:在哪登录、注册入口缺失、登录后能否传到生产后台——当前<strong>最高优先级阻塞</strong>。</div></div>
|
||||
<div class="priority"><span class="rank">2</span><div><strong>移动端安装包与真机验证</strong> · <span style="color:var(--warn)">进行中</span><br>多次索要安装路径、重装、确认特殊设备可用——移动侧是验收主战场。</div></div>
|
||||
<div class="priority"><span class="rank">3</span><div><strong>开发与生产环境数据一致</strong> · <span style="color:var(--warn)">痛点明显</span><br>希望环境统一,避免「本地有、线上没有」导致判断失真与信任损耗。</div></div>
|
||||
<div class="priority"><span class="rank">4</span><div><strong>输入体验:从逐字到整句</strong> · <span style="color:var(--warm)">已识别</span><br>主动提出「如何判断一句完整的话」——意识到原始按键流对 AI 分析不友好,属于<strong>体验债</strong>。</div></div>
|
||||
<div class="priority"><span class="rank">5</span><div><strong>界面设计与 AI 产品方向</strong> · <span style="color:var(--ok)">方向已定</span><br>明确要做 AI 原生界面,后续与智能平台对接——战略清晰,待执行。</div></div>
|
||||
<div class="priority"><span class="rank">6</span><div><strong>团队协同与进度对齐</strong> · <span style="color:var(--ok)">常规进行</span><br>在协作 IM 中询问同事休假、机器环境——重要事项包含<strong>人的可用性</strong>,不单是代码。</div></div>
|
||||
</div>
|
||||
|
||||
<h2>三、偏好与价值取向</h2>
|
||||
<div class="card">
|
||||
<div class="two-col">
|
||||
<ul>
|
||||
<li><strong>产品审美:</strong>AI 原生,非传统工具堆砌</li>
|
||||
<li><strong>架构取舍:</strong>账号体系先独立,边界清晰,避免过早耦合</li>
|
||||
<li><strong>质量观:</strong>先证明链路通,再谈功能丰富</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><strong>决策风格:</strong>确认型——要「确定进了库」才往下走</li>
|
||||
<li><strong>表达习惯:</strong>短句、口语、效率优先</li>
|
||||
<li><strong>信任机制:</strong>亲眼所见 > 口头承诺</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="quote">「一定要往 AI 方面设计,后续对接智能平台」—— 产品战略与审美方向的明确表态。</div>
|
||||
<div class="quote">「暂时不要打通主产品用户体系」—— 倾向独立演进、可控边界。</div>
|
||||
</div>
|
||||
|
||||
<h2>四、决策与沟通模式</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><strong>决策链路:</strong>提出假设 → 亲自或委托验证 → 看到数据/界面 → 才进入下一步。极少「先发布再验证」。</li>
|
||||
<li><strong>沟通风格:</strong>指令式短句为主(「试一下」「你去后台看」「路径发给我」),信息密度高,省略客套。</li>
|
||||
<li><strong>追问模式:</strong>同一主题多次出现(登录、后台、安装包),说明<strong>未获满意答案前不会切换话题</strong>。</li>
|
||||
<li><strong>协作方式:</strong>深度工作在开发工具内完成,协调工作在 IM 内完成——<strong>双通道不混用</strong>,但围绕同一项目目标。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>五、忙闲节律(何时最高效)</h2>
|
||||
<div class="card">
|
||||
<p><strong>峰值时段:${esc(peak?.[0] ?? '05:30')} 前后</strong>(约 15 分钟内 ${peak?.[1] ?? 79} 次输入),整体为<strong>约 1 小时的凌晨攻坚</strong>,形态是「阻塞清零」而非均匀分布。</p>
|
||||
<div class="heat">
|
||||
${Object.entries(buckets15)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([t, n]) => `<span class="${t === peak?.[0] ? 'hot' : ''}">${t} · ${n} 次</span>`)
|
||||
.join('')}
|
||||
</div>
|
||||
<p style="margin-top:14px;color:var(--muted);font-size:.88rem">更早时段有一次较轻的验证性输入,强度远低于凌晨段——<strong>忙闲分明</strong>:白天/傍晚零散试,深夜集中解决问题。</p>
|
||||
<p style="margin-top:10px;font-size:.9rem"><strong>工作节类型:</strong>冲刺型 — 遇到阻塞会集中一段长时间清零,而非碎片化推进。</p>
|
||||
</div>
|
||||
|
||||
<h2>六、工具与场景分布</h2>
|
||||
<div class="card">
|
||||
<p style="margin-bottom:14px">有效语义片段共 ${meaningful.length} 段,场景分布如下:</p>
|
||||
<div class="bar-row"><span class="lbl">深度开发</span><div class="track"><div class="fill" style="width:${devPct}%"></div></div><span class="pct">${devPct}%</span></div>
|
||||
<div class="bar-row"><span class="lbl">协作沟通</span><div class="track"><div class="fill" style="width:${collabPct}%"></div></div><span class="pct">${collabPct}%</span></div>
|
||||
<div class="bar-row"><span class="lbl">其他</span><div class="track"><div class="fill" style="width:${otherPct}%"></div></div><span class="pct">${otherPct}%</span></div>
|
||||
<p style="margin-top:14px;font-size:.88rem;color:var(--muted)">深度开发侧约 ${cursorSegs.length} 段(技术推进、验收指令);协作侧约 ${wecomSegs.length} 段(进度对齐、资源协调)。</p>
|
||||
</div>
|
||||
|
||||
<h2>七、行为模式标签</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><strong>阻塞驱动:</strong>输入高峰跟在「看不到 / 登不上 / 传不到」之后——忙是因为在清障。</li>
|
||||
<li><strong>验收先于发布:</strong>反复安装、重装、对库——先证明链路通。</li>
|
||||
<li><strong>愿景与落地同屏:</strong>谈 AI 战略的同时谈安装包路径与环境一致。</li>
|
||||
<li><strong>不孤立作战:</strong>自己试 + 指挥他人验证,团队是延伸感官。</li>
|
||||
<li><strong>厌恶模糊态:</strong>「有没有进库」「能不能登录」必须得到是/否。</li>
|
||||
<li><strong>体验敏感:</strong>能指出「逐字输入」对产品化的影响——具备元认知。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>八、风险与阻塞点</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><strong>账号链路未闭环:</strong>登录入口、注册流程、后台关联任一环节断裂,都会卡住全部验收。</li>
|
||||
<li><strong>环境不一致:</strong>本地与生产数据不同步,导致「试了白试」的信任危机。</li>
|
||||
<li><strong>输入粒度太细:</strong>逐字上报增加分析噪声,拖慢 AI 回忆与画像质量。</li>
|
||||
<li><strong>深夜攻坚可持续性问题:</strong>高峰在凌晨,长期可能带来疲劳与决策质量波动(需关注,非批评)。</li>
|
||||
<li><strong>协调依赖:</strong>部分验证需他人配合(后台查看、环境确认),存在<strong>外部等待</strong>风险。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>九、个性化效率建议</h2>
|
||||
<div class="card">
|
||||
<div class="suggest"><b>① 登录链路一键诊断</b><p>提供「从安装 → 注册/登录 → 后台可见」的检查清单,每步给出是/否,减少反复追问。</p></div>
|
||||
<div class="suggest"><b>② 凌晨高峰前预置上下文</b><p>在活跃时段开始前,自动汇总「昨日未闭环事项 + 今日待验收项」,进入即可攻坚。</p></div>
|
||||
<div class="suggest"><b>③ 跨工具线程视图</b><p>将开发工具内的技术指令与 IM 里的协调消息合并为同一项目时间线,免手动拼图。</p></div>
|
||||
<div class="suggest"><b>④ 句子级输入聚合</b><p>优先落地「停顿切分 + 整句展示」,提升后续 AI 分析与回顾体验——用户已主动提出此需求。</p></div>
|
||||
<div class="suggest"><b>⑤ 环境一致性看板</b><p>用单一视图对比「本地 / 预发 / 生产」关键数据是否一致,回答「到底有没有进库」。</p></div>
|
||||
</div>
|
||||
|
||||
<h2>十、一句话总结</h2>
|
||||
<div class="card">
|
||||
<p style="font-size:1.08rem;color:var(--text)"><strong>用户A</strong>是一位<strong>深夜高效、结果导向的 AI 产品负责人</strong>:当前最重要的事是<strong>移动端登录可靠、后台数据可见、环境一致</strong>;偏好<strong>独立产品 + AI 体验</strong>;最高效在<strong>凌晨深专注段</strong>;做任何决定前都要<strong>亲眼确认</strong>;适合用<strong>清单化验收 + 跨工具线程</strong>来提升效率。</p>
|
||||
</div>
|
||||
|
||||
<h2>附:推断依据(代表性原话 · 已脱敏)</h2>
|
||||
<div class="card" style="font-size:.86rem;color:var(--muted)">
|
||||
<ul>
|
||||
${meaningful
|
||||
.filter((s) => s.text.length > 8)
|
||||
.slice(0, 14)
|
||||
.map(
|
||||
(s) =>
|
||||
`<li>${esc(fmtCst(s.start))} · ${esc(s.text.slice(0, 80))}${s.text.length > 80 ? '…' : ''}</li>`,
|
||||
)
|
||||
.join('')}
|
||||
</ul>
|
||||
<p style="margin-top:12px;font-size:.82rem">以上为语义归纳附录,完整报告主体见上文十维画像。</p>
|
||||
</div>
|
||||
|
||||
<footer>MeInput 全景用户画像 · 语义归纳 · 非工程日志 · ${esc(new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }))}</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, html, 'utf8');
|
||||
|
||||
const memindPool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
|
||||
await markPageDeliveryContractReady({
|
||||
pool: memindPool,
|
||||
userId: ownerId,
|
||||
relativePath: 'public/behavior-pattern-analysis.html',
|
||||
}).catch(() => {});
|
||||
|
||||
console.log('Updated portrait page:', outPath);
|
||||
await mePool.end();
|
||||
await memindPool.end();
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify meinput temporal recall → chat injection for a logged-in user.
|
||||
*
|
||||
* Usage:
|
||||
* MEMIND_BASE_URL=http://127.0.0.1:8081 \
|
||||
* MEMIND_USERNAME=john MEMIND_PASSWORD=981122tj \
|
||||
* node scripts/verify-meinput-recall-chat.mjs
|
||||
*/
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import {
|
||||
createAgentRun,
|
||||
createReporter,
|
||||
extractAssistantTexts,
|
||||
getSession,
|
||||
loginViaApi,
|
||||
resolvePortalBase,
|
||||
waitForAssistantGrowth,
|
||||
waitForRunTerminal,
|
||||
} from './scenario-test-lib.mjs';
|
||||
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const QUERY = process.env.MEINPUT_VERIFY_QUERY ?? '最近我输入了什么?';
|
||||
const baseUrl = (process.env.MEMIND_BASE_URL ?? resolvePortalBase(Number(process.env.H5_PORT ?? 8081))).replace(/\/$/, '');
|
||||
const account = {
|
||||
username: process.env.MEMIND_USERNAME ?? process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john',
|
||||
password: process.env.MEMIND_PASSWORD ?? process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '981122tj',
|
||||
};
|
||||
|
||||
async function verifyTemporalRecallApi(cookie) {
|
||||
const headers = { 'content-type': 'application/json', cookie };
|
||||
const planRes = await fetch(`${baseUrl}/api/v1/context/plan`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ query: QUERY, now: new Date().toISOString() }),
|
||||
});
|
||||
const planBody = await planRes.json();
|
||||
if (!planRes.ok) throw new Error(`context/plan ${planRes.status}: ${JSON.stringify(planBody)}`);
|
||||
|
||||
const recallRes = await fetch(`${baseUrl}/api/v1/temporal-recall/query`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ plan: planBody.plan, limit: 20 }),
|
||||
});
|
||||
const recallBody = await recallRes.json();
|
||||
if (!recallRes.ok) throw new Error(`temporal-recall/query ${recallRes.status}: ${JSON.stringify(recallBody)}`);
|
||||
|
||||
const meinputItems = [];
|
||||
for (const group of recallBody.groups ?? []) {
|
||||
for (const item of group.items ?? []) {
|
||||
if (item.source === 'meinput') meinputItems.push(item);
|
||||
}
|
||||
}
|
||||
for (const item of recallBody.items ?? []) {
|
||||
if (item.source === 'meinput') meinputItems.push(item);
|
||||
}
|
||||
|
||||
return {
|
||||
plan: planBody.plan,
|
||||
stats: recallBody.stats,
|
||||
meinputItems,
|
||||
sampleTitles: meinputItems.slice(0, 5).map((item) => item.title?.slice(0, 40)),
|
||||
};
|
||||
}
|
||||
|
||||
function replyLooksGrounded(text, meinputSamples = []) {
|
||||
const normalized = String(text ?? '');
|
||||
if (/meinput|\[meinput\]|输入记录|按键|键盘输入/u.test(normalized)) return true;
|
||||
const keywords = ['项目', '手机', '越狱', '安装', '2026-09-02', '19:4'];
|
||||
if (keywords.some((kw) => normalized.includes(kw))) return true;
|
||||
return meinputSamples.some((title) => title && normalized.includes(String(title).slice(0, 4)));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reporter = createReporter();
|
||||
console.log(`==> meinput recall chat verify`);
|
||||
console.log(` Portal: ${baseUrl}`);
|
||||
console.log(` User: ${account.username}`);
|
||||
console.log(` Query: ${QUERY}\n`);
|
||||
|
||||
const statusRes = await fetch(`${baseUrl}/auth/status`);
|
||||
if (!statusRes.ok) throw new Error(`Portal 未就绪: ${statusRes.status}`);
|
||||
|
||||
const auth = await loginViaApi(baseUrl, account, reporter);
|
||||
const api = await verifyTemporalRecallApi(auth.cookie);
|
||||
|
||||
reporter.pass(
|
||||
'API plan 识别 temporal recall',
|
||||
`${api.plan?.query_type ?? 'unknown'} / meinput=${api.plan?.sources?.meinput ?? '?'}`,
|
||||
);
|
||||
|
||||
if (!(api.stats?.sources_queried ?? []).includes('meinput')) {
|
||||
reporter.fail('API 检索源', `未查询 meinput: ${JSON.stringify(api.stats?.sources_queried)}`);
|
||||
} else {
|
||||
reporter.pass('API 检索源', 'meinput 已参与查询');
|
||||
}
|
||||
|
||||
if ((api.stats?.raw_count ?? 0) <= 0) {
|
||||
reporter.fail('API raw_count', 'meinput/chat 未返回原始条目');
|
||||
} else {
|
||||
reporter.pass('API raw_count', String(api.stats.raw_count));
|
||||
}
|
||||
|
||||
if ((api.stats?.returned_count ?? 0) <= 0) {
|
||||
reporter.fail('API returned_count', '排序后无条目可注入');
|
||||
} else {
|
||||
reporter.pass('API returned_count', String(api.stats.returned_count));
|
||||
}
|
||||
|
||||
if (api.meinputItems.length <= 0) {
|
||||
reporter.fail('API meinput 条目', 'groups/items 中无 meinput 源数据');
|
||||
} else {
|
||||
reporter.pass('API meinput 条目', `${api.meinputItems.length} 条,样例: ${api.sampleTitles.join(' | ')}`);
|
||||
}
|
||||
|
||||
const run = await createAgentRun(baseUrl, auth.cookie, { message: QUERY });
|
||||
reporter.pass('发起聊天 run', run.runId);
|
||||
|
||||
const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, 180000);
|
||||
if (terminal.status !== 'succeeded') {
|
||||
reporter.fail('聊天 run 终态', terminal.status ?? 'unknown');
|
||||
} else {
|
||||
reporter.pass('聊天 run 终态', 'succeeded');
|
||||
}
|
||||
|
||||
const sessionId = run.sessionId ?? terminal.sessionId ?? terminal.agent_session_id;
|
||||
if (!sessionId) {
|
||||
reporter.fail('会话 ID', 'run 未返回 sessionId');
|
||||
} else {
|
||||
const growth = await waitForAssistantGrowth(baseUrl, auth.cookie, sessionId, {
|
||||
minChars: 20,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
const session = growth ? { ok: true, session: { conversation: [] } } : await getSession(baseUrl, auth.cookie, sessionId);
|
||||
const texts = growth?.texts ?? extractAssistantTexts(session.session ?? session.payload ?? session);
|
||||
const reply = (growth?.combined ?? texts.join('\n')).trim();
|
||||
|
||||
if (!reply) {
|
||||
reporter.fail('助手回复', '会话中无 assistant 文本');
|
||||
} else {
|
||||
reporter.pass('助手回复长度', `${reply.length} 字符`);
|
||||
console.log('\n--- assistant preview ---');
|
||||
console.log(reply.slice(0, 600));
|
||||
console.log('--- end preview ---\n');
|
||||
|
||||
if (replyLooksGrounded(reply, api.sampleTitles)) {
|
||||
reporter.pass('回复 grounded', '包含 meinput/输入记录或样例关键词');
|
||||
} else {
|
||||
reporter.fail(
|
||||
'回复 grounded',
|
||||
'未检测到 meinput 注入痕迹(项目/手机/输入记录等)',
|
||||
);
|
||||
}
|
||||
|
||||
if (/无法直接访问|不能确切知道|没有权限访问/u.test(reply) && !replyLooksGrounded(reply, api.sampleTitles)) {
|
||||
reporter.fail('空注入幻觉', '模型声称无法访问记录,但 API 明明有 meinput 数据');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify Rain mode: full meinput load + LLM → Goose handoff.
|
||||
*
|
||||
* Usage:
|
||||
* MEMIND_USERNAME=john MEMIND_PASSWORD=981122tj node scripts/verify-rain-chat.mjs
|
||||
*/
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import {
|
||||
createAgentRun,
|
||||
createReporter,
|
||||
extractAssistantTexts,
|
||||
getSession,
|
||||
loginViaApi,
|
||||
resolvePortalBase,
|
||||
waitForAssistantGrowth,
|
||||
waitForRunTerminal,
|
||||
} from './scenario-test-lib.mjs';
|
||||
import { RAIN_SKILL_NAME } from '../chat-skills.mjs';
|
||||
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const QUERY = process.env.RAIN_VERIFY_QUERY ?? '最近我输入了什么?';
|
||||
const baseUrl = (process.env.MEMIND_BASE_URL ?? resolvePortalBase(Number(process.env.H5_PORT ?? 8081))).replace(/\/$/, '');
|
||||
const account = {
|
||||
username: process.env.MEMIND_USERNAME ?? 'john',
|
||||
password: process.env.MEMIND_PASSWORD ?? process.env.JOHN_PASSWORD ?? '981122tj',
|
||||
};
|
||||
|
||||
|
||||
async function main() {
|
||||
const reporter = createReporter();
|
||||
console.log(`==> Rain verify\n Portal: ${baseUrl}\n User: ${account.username}\n Query: ${QUERY}\n`);
|
||||
|
||||
const auth = await loginViaApi(baseUrl, account, reporter);
|
||||
const run = await createAgentRun(baseUrl, auth.cookie, {
|
||||
message: QUERY,
|
||||
selectedChatSkill: RAIN_SKILL_NAME,
|
||||
});
|
||||
|
||||
// Patch message shape for rain metadata (createAgentRun uses buildUserMessage)
|
||||
reporter.pass('发起 Rain run', run.runId);
|
||||
|
||||
const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, 180000);
|
||||
if (terminal.status !== 'succeeded') {
|
||||
const errText = String(terminal.error ?? terminal.lastError ?? '');
|
||||
if (/Arrearage|Access denied/i.test(errText)) {
|
||||
reporter.fail('run 终态', `LLM 账户欠费/不可用,非 Rain 逻辑错误:${errText.slice(0, 120)}`);
|
||||
} else {
|
||||
reporter.fail('run 终态', terminal.status ?? errText.slice(0, 120) ?? 'unknown');
|
||||
}
|
||||
} else {
|
||||
reporter.pass('run 终态', 'succeeded');
|
||||
}
|
||||
|
||||
const sessionId = run.sessionId ?? terminal.sessionId ?? terminal.agent_session_id;
|
||||
const session = await getSession(baseUrl, auth.cookie, sessionId);
|
||||
const texts = extractAssistantTexts(session.session ?? session.payload ?? session);
|
||||
const reply = texts.join('\n').trim();
|
||||
|
||||
if (!reply) {
|
||||
reporter.fail('助手回复', '空');
|
||||
} else {
|
||||
console.log('\n--- assistant ---\n', reply.slice(0, 800), '\n---\n');
|
||||
reporter.pass('助手回复', `${reply.length} 字符`);
|
||||
if (/疲惫|车载冰箱|便签页面内容对所有人可见|TKMind 记忆系统/u.test(reply) && !/MeInput|输入|项目|手机|Rain/u.test(reply)) {
|
||||
reporter.fail('注入隔离', '回复像 Memory V2,未体现 MeInput');
|
||||
} else {
|
||||
reporter.pass('注入隔离', '未检测到典型长期记忆污染');
|
||||
}
|
||||
if (/0\.\d{4}/.test(reply) && /关联值|评分|权重/u.test(reply)) {
|
||||
reporter.fail('分数泄漏', '回复含 recall 分数语义');
|
||||
} else {
|
||||
reporter.pass('分数泄漏', '未检测到');
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user