Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

269 lines
12 KiB
JavaScript

// Deep-audit gap closers for Kimi (kimi.com).
//
// Discovered via direct DOM enumeration across mode pages (/slides, /docs,
// /deep-research, /agent, /settings, /chat/history) — these wrap buttons
// not covered by the initial chat/ui/storage commands:
//
// sign-out — click SignOut svg (in /settings page)
// upgrade — click Upgrade button (sidebar bottom)
// dismiss-banner — close the "Make a Review & Earn Credit" or
// "获取应用程序" banner
// templates [--mode] — list template cards on a mode page (PPT /docs/
// deep-research / agent — each shows curated
// example projects)
// history-edit <chat-id> <new-title> — click the per-row Edit button
// on /chat/history (each conv has an inline Edit)
// user-rules — read/write the rules from /settings (best-effort)
import { cli, Strategy } from '@jackwener/opencli/registry';
import {
ArgumentError,
CommandExecutionError,
EmptyResultError,
} from '@jackwener/opencli/errors';
import {
KIMI_DOMAIN,
KIMI_URL,
IS_VISIBLE_JS,
ensureOnKimi,
parseChatId,
clickBySvgNameScript,
} from './_utils.js';
const AUDIT_EXTRA_COLUMNS = ['Status', 'Index', 'Category', 'Title', 'ChatId', 'NewTitle'];
// -------- sign-out --------
cli({
site: 'kimi',
name: 'sign-out',
access: 'write',
description: 'Click SignOut on the Kimi /settings page. Navigates to /settings first if not already there. Requires --yes.',
domain: KIMI_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
args: [
{ name: 'yes', type: 'boolean', default: false, help: 'Actually sign out (default: dry-run)' },
],
columns: AUDIT_EXTRA_COLUMNS,
func: async (page, kwargs) => {
const yes = kwargs?.yes === true || kwargs?.yes === 'true' || kwargs?.yes === '1';
if (!yes) {
return [{ Status: 'dry-run — pass --yes to actually sign out' }];
}
const url = await page.evaluate('window.location.href');
if (!String(url || '').includes('/settings')) {
await page.goto(`${KIMI_URL}settings`);
await page.wait(1.5);
}
const res = await page.evaluate(clickBySvgNameScript('SignOut'));
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'SignOut svg not found', '');
await page.wait(1);
return [{ Status: 'signed-out' }];
},
});
// -------- upgrade --------
cli({
site: 'kimi',
name: 'upgrade',
access: 'write',
description: 'Click the "Upgrade" button (or 升级会员) in the Kimi sidebar — opens the membership/upgrade page or dialog.',
domain: KIMI_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
args: [],
columns: AUDIT_EXTRA_COLUMNS,
func: async (page) => {
await ensureOnKimi(page);
const res = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const btns = Array.from(document.querySelectorAll('button, [role="button"], a')).filter(isVisible);
const target = btns.find((b) => {
const t = (b.innerText || b.textContent || '').trim();
return /^Upgrade$|^升级|^会员计划|^开通会员/i.test(t) && t.length < 30;
});
if (!target) return { ok: false, reason: 'No Upgrade-style button visible.' };
const r = target.getBoundingClientRect();
const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 };
target.dispatchEvent(new PointerEvent('pointerdown', opts));
target.dispatchEvent(new MouseEvent('mousedown', opts));
target.dispatchEvent(new PointerEvent('pointerup', opts));
target.dispatchEvent(new MouseEvent('mouseup', opts));
target.click();
return { ok: true };
})()`);
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'upgrade click failed', '');
await page.wait(0.6);
return [{ Status: 'clicked' }];
},
});
// -------- dismiss-banner --------
cli({
site: 'kimi',
name: 'dismiss-banner',
access: 'write',
description: 'Close any visible sidebar banner (e.g., "Make a Review & Earn Credit", "获取应用程序") by clicking its Close svg.',
domain: KIMI_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
args: [],
columns: AUDIT_EXTRA_COLUMNS,
func: async (page) => {
await ensureOnKimi(page);
const res = await page.evaluate(clickBySvgNameScript('Close', { last: false }));
if (!res?.ok) {
throw new EmptyResultError('kimi dismiss-banner', 'No Close svg visible — no banner to dismiss?');
}
return [{ Status: 'dismissed' }];
},
});
// -------- templates --------
cli({
site: 'kimi',
name: 'templates',
access: 'read',
description: 'List template cards visible on a Kimi mode page (PPT/docs/deep-research/agent). Each mode shows curated example projects organized by category. Pass --mode to navigate first.',
domain: KIMI_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
args: [
{ name: 'mode', required: false, help: 'Navigate to mode first: ppt|docs|deep-research|agent|websites|sheets|agent-swarm|code' },
{ name: 'limit', type: 'int', required: false, default: 30 },
],
columns: AUDIT_EXTRA_COLUMNS,
func: async (page, kwargs) => {
const mode = String(kwargs?.mode || '').trim().toLowerCase();
const modeMap = {
ppt: '/slides',
docs: '/docs',
'deep-research': '/deep-research',
agent: '/agent',
websites: '/websites',
sheets: '/sheets',
'agent-swarm': '/agent-swarm',
code: '/code',
};
if (mode) {
const target = modeMap[mode];
if (!target) throw new ArgumentError('mode', `unknown mode "${mode}"`);
await page.goto(`${KIMI_URL}${target.slice(1)}`);
await page.wait(2);
} else {
await ensureOnKimi(page);
}
// Templates: visible <a> or [role=button] whose text follows the
// pattern "category\n\ntitle" (e.g., "商业财经\n\n宁德时代财报分析").
const cards = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const els = Array.from(document.querySelectorAll('a, [role="button"], button')).filter(isVisible);
const out = [];
for (const el of els) {
const tx = (el.innerText || '').trim();
// Match "category\\n\\ntitle" or "category\\ntitle" patterns
const m = tx.match(/^([^\\n]+)\\n+(.+)$/);
if (m && m[1].length < 30 && m[2].length < 100 && m[1] !== m[2]) {
out.push({ category: m[1].trim(), title: m[2].trim() });
}
}
// Dedupe by title
const seen = new Set();
return out.filter((c) => {
if (seen.has(c.title)) return false;
seen.add(c.title);
return true;
});
})()`);
if (!cards.length) {
throw new EmptyResultError('kimi templates', 'No template cards visible. Are you on a mode page?');
}
const limit = Number.isInteger(kwargs?.limit) && kwargs.limit > 0 ? kwargs.limit : 30;
return cards.slice(0, limit).map((c, i) => ({ Index: i + 1, Category: c.category, Title: c.title }));
},
});
// -------- history-rename --------
cli({
site: 'kimi',
name: 'history-rename',
access: 'write',
description: 'Rename a chat from the /chat/history page (clicks the inline Edit svg next to a chat row, types the new title, and saves). Requires --yes.',
domain: KIMI_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
args: [
{ name: 'chat-id', positional: true, required: true, help: 'Chat id (UUID-like)' },
{ name: 'new-title', positional: true, required: true, help: 'New title' },
{ name: 'yes', type: 'boolean', default: false, help: 'Actually rename (default: dry-run)' },
],
columns: AUDIT_EXTRA_COLUMNS,
func: async (page, kwargs) => {
const id = parseChatId(kwargs?.['chat-id']);
const newTitle = String(kwargs?.['new-title'] || '').trim();
if (!id) throw new ArgumentError('chat-id', 'is required');
if (!newTitle) throw new ArgumentError('new-title', 'is required');
const yes = kwargs?.yes === true || kwargs?.yes === 'true' || kwargs?.yes === '1';
if (!yes) {
return [{ Status: 'dry-run — pass --yes to rename', ChatId: id, NewTitle: newTitle }];
}
// Navigate to history page
await page.goto(`${KIMI_URL}chat/history`);
await page.wait(2);
// Find the row with href containing the chat id, then click its Edit svg
const clickRes = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const row = Array.from(document.querySelectorAll('a[href*="/chat/' + ${JSON.stringify(id)} + '"]')).find(isVisible);
if (!row) return { ok: false, reason: 'Chat row not found in history.' };
// The Edit svg is in a sibling or descendant.
let container = row.parentElement || row;
for (let i = 0; i < 4 && container.parentElement; i++) container = container.parentElement;
const editSvg = container.querySelector('svg[name="Edit"]');
if (!editSvg) return { ok: false, reason: 'Edit svg not found near chat row.' };
let editBtn = editSvg;
for (let i = 0; i < 5 && editBtn.parentElement; i++) { editBtn = editBtn.parentElement; if (editBtn.getAttribute('role') === 'button' || editBtn.tagName === 'BUTTON') break; }
editBtn.click();
return { ok: true };
})()`);
if (!clickRes?.ok) throw new CommandExecutionError(clickRes?.reason || 'Edit click failed', '');
await page.wait(0.5);
// Fill the inline input + submit via Enter
const fillRes = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const inputs = Array.from(document.querySelectorAll('input[type="text"], input:not([type])')).filter(isVisible);
const input = inputs[inputs.length - 1];
if (!input) return { ok: false, reason: 'No input mounted after clicking Edit.' };
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(input, ${JSON.stringify(newTitle)});
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
return { ok: true };
})()`);
if (!fillRes?.ok) throw new CommandExecutionError(fillRes?.reason || 'Rename input fill failed', '');
await page.wait(1);
const verified = await page.evaluate(`(() => {
const row = Array.from(document.querySelectorAll('a[href*="/chat/' + ${JSON.stringify(id)} + '"]'))
.find((el) => (el.innerText || el.textContent || '').includes(${JSON.stringify(newTitle)}));
return !!row;
})()`);
if (!verified) {
throw new CommandExecutionError(
`Kimi history rename was not verified for chat ${id}`,
'The edit input was submitted, but the history row did not show the requested title.',
);
}
return [{ Status: 'renamed', ChatId: id, NewTitle: newTitle }];
},
});