Compare commits

...

1 Commits

Author SHA1 Message Date
john 884e819f70 fix(mindspace): lower page access password minimum to 6 characters
Align publication validation, portal gate HTML, H5 publish UI, MCP docs,
and page-data skill guidance so passwords like 888888 are accepted while
3-character values such as 888 remain rejected.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 09:54:46 +08:00
9 changed files with 31 additions and 19 deletions
+14 -3
View File
@@ -78,11 +78,20 @@ function normalizeAccessMode(value) {
return mode;
}
export const PAGE_ACCESS_PASSWORD_MIN_LENGTH = 6;
export const PAGE_ACCESS_PASSWORD_MAX_LENGTH = 128;
function normalizePassword(value, required) {
const password = String(value ?? '');
if (!required && !password) return null;
if (password.length < 8 || password.length > 128) {
throw publicationError('访问密码长度必须为 8 到 128 个字符', 'invalid_publish_input');
if (
password.length < PAGE_ACCESS_PASSWORD_MIN_LENGTH
|| password.length > PAGE_ACCESS_PASSWORD_MAX_LENGTH
) {
throw publicationError(
`访问密码长度必须为 ${PAGE_ACCESS_PASSWORD_MIN_LENGTH}${PAGE_ACCESS_PASSWORD_MAX_LENGTH} 个字符`,
'invalid_publish_input',
);
}
return password;
}
@@ -1083,7 +1092,7 @@ export function createPublicationService(pool, options = {}) {
} else if (publication.password_hash) {
passwordHash = publication.password_hash;
} else {
throw publicationError('访问密码长度必须为 8 到 128 个字符', 'invalid_publish_input');
throw publicationError('口令访问模式必须提供访问密码', 'invalid_publish_input');
}
}
const now = Date.now();
@@ -1498,6 +1507,8 @@ async function registerPublicationArtifactForConversation({
}
export const publicationInternals = {
PAGE_ACCESS_PASSWORD_MIN_LENGTH,
PAGE_ACCESS_PASSWORD_MAX_LENGTH,
normalizeSlug,
normalizeAccessMode,
normalizePassword,
+1
View File
@@ -110,6 +110,7 @@ test('hashes access passwords with a random salt', () => {
() => publicationInternals.normalizePassword('short', true),
(error) => error.code === 'invalid_publish_input',
);
assert.equal(publicationInternals.normalizePassword('888888', true), '888888');
});
test('requires future expiry timestamps for time-limited pages', () => {
+1 -1
View File
@@ -473,7 +473,7 @@ const ALL_TOOLS = [
password: {
type: 'string',
description:
'口令访问密码,accessMode=password 时可选;未提供则默认 88888888(至少 8 位)',
'口令访问密码,accessMode=password 时可选;未提供则默认 88888888(至少 6 位)',
},
urlSlug: { type: 'string', description: '发布 URL slug,可选' },
datasets: {
+1 -1
View File
@@ -45,7 +45,7 @@ const SUGGESTION_BY_CODE = Object.freeze({
title: '页面数据绑定或发布失败',
actions: Object.freeze([
'确认 dataset 已 private_data_register_dataset 注册',
'对每个公开页调用 private_data_bind_workspace_page(含 password 策略时口令 ≥8 位)',
'对每个公开页调用 private_data_bind_workspace_page(含 password 策略时口令 ≥6 位)',
'检查 .mindspace/page-data-policies 是否生成且列白名单与表单字段一致',
]),
}),
+1 -1
View File
@@ -22,7 +22,7 @@ export function resolvePageDataBindAccess(accessMode = 'public') {
return publicationInternals.normalizeAccessMode(accessMode);
}
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD<8 位抛错 */
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD<6 位抛错 */
export function resolvePageDataBindPassword(accessMode, password = null) {
const mode = resolvePageDataBindAccess(accessMode);
if (mode !== 'password') return null;
+1 -1
View File
@@ -39,7 +39,7 @@ export function renderPublicationPasswordGate(
<form method="post" action="${action}">
<h1>此页面受密码保护</h1>
<p>请输入发布者提供的访问密码。密码不会写入链接或浏览器日志。</p>
<input type="password" name="password" minlength="8" maxlength="128" required autocomplete="current-password">
<input type="password" name="password" minlength="6" maxlength="128" required autocomplete="current-password">
<button type="submit">访问页面</button>
</form>
</body>
+9 -9
View File
@@ -46,7 +46,7 @@ HTML 页面只能调用 Page Data APIpage-data-client.js);
|----|--------|
| 访客 | 匿名提交(`public`,仅 `insert` |
| 后台 | **独立** HTML 页(`password`,仅 `read` |
| 后台口令 | **`88888888`**(平台要求 8~128 位;用户可指定其它合法口令覆盖) |
| 后台口令 | **`88888888`**(平台要求 6~128 位;用户可指定其它合法口令覆盖,如 `888888` |
| 提交后修改 | 不支持(一次性) |
| 页面 | `public/*-survey.html` + `public/*-admin.html`(两文件各 bind 一次) |
@@ -54,7 +54,7 @@ HTML 页面只能调用 Page Data APIpage-data-client.js);
口令规则:
- **禁止**接受或使用 <8 位口令(如 `888`);若用户坚持短口令,说明平台限制并代用 `88888888` 或请其给出 ≥8 位。
- **禁止**接受或使用 <6 位口令(如 `888`);若用户坚持短口令,说明平台限制并代用 `888888` / `88888888` 或请其给出 ≥6 位。
- bind 时 **`password` 必须传入**且会写入发布记录;禁止只改 `access_mode` 不写 `password_hash`
- 交付说明写明后台口令;用户可在 MindSpace「页面数据」面板重置。
@@ -73,7 +73,7 @@ HTML 页面只能调用 Page Data APIpage-data-client.js);
**仅以下情况才问用户(每次最多 1~2 点)**
1. 表述同时命中两个互斥分支(如「匿名提交」+「同页内嵌口令看全量」)→ 给 A/E 选项说明须拆页
2. 用户明确要的口令 <8 位 → 请改口令或确认用默认 `88888888`
2. 用户明确要的口令 <6 位 → 请改口令或确认用默认 `88888888`
3. 需要 B/C 但表结构是否要 `created_by_user_id` 等列尚不清楚 → 确认登录隔离
**禁止**连续多轮只输出「我先检查工作区/加载技能」而不调用工具;**第一轮工具**应是 `load_skill``list_dir` / `private_data_execute`,不是空计划。
@@ -82,7 +82,7 @@ HTML 页面只能调用 Page Data APIpage-data-client.js);
```text
- public:可匿名 insert;服务端禁止 update / softDelete
- password:所有 API 须先 authenticate;口令 ≥8 位且须写入发布记录
- password:所有 API 须先 authenticate;口令 ≥6 位且须写入发布记录
- `public` 策略中的 `insert: true` 等价于任何访客都能直接调用 API 写入;前端口令、隐藏按钮和 JavaScript 判断都不是权限控制
- password 页面必须调用 `client.authenticate(password)` 获取服务端 token;禁止在 HTML 中硬编码口令或只做 `password === "..."` 比较
- 同一公开页若要求「所有人可评价、只有所有者可写正文」:正文 dataset 在公开页必须 `insert: false`,评价 dataset 才能 `insert: true`;所有者写正文必须使用独立 `password` 作者页或 `login_required` 页面
@@ -99,7 +99,7 @@ HTML 页面只能调用 Page Data APIpage-data-client.js);
- 分支:A 匿名提交 + 独立口令后台
- 访客:匿名提交(public,仅 insert
- 管理员:独立后台页(password,仅 read
- 后台口令:88888888(可改为你的 ≥8 位口令)
- 后台口令:88888888(可改为你的 ≥6 位口令,如 888888
- 提交后修改:不支持
- 页面:public/xxx.html + public/xxx-admin.html
- 内容:(由你的描述生成题目/字段/报表)
@@ -283,7 +283,7 @@ await client.deleteRow('dataset_name', rowId);
若问卷与后台是多个 HTML 文件,对每个 `public/*.html` 各调用一次 bind
- **问卷页**`accessMode: "public"` + dataset 仅 `insert`
- **后台页**`accessMode: "password"` + dataset 仅 `read`(发布口令至少 8 位)
- **后台页**`accessMode: "password"` + dataset 仅 `read`(发布口令至少 6 位)
**顺序**:先 `write_file` 完整 HTML → 再 `bind`;禁止只写「问卷页面」占位文字就发布。
@@ -301,7 +301,7 @@ await client.deleteRow('dataset_name', rowId);
// pageId 可省略:平台访问时注入 __MINDSPACE_PAGE_DATA__
const client = MindSpacePageData.createClient({ apiBase: '/api' });
// 口令页后台:await client.authenticate('88888888'); // 口令 ≥8 位,与发布时一致
// 口令页后台:await client.authenticate('88888888'); // 口令 ≥6 位,与发布时一致
// 提交:await client.insertRow('survey_responses', { q1_feature: '...', ... });
// 列表:const { rows } = await client.listRows('survey_responses', { limit: 50 });
</script>
@@ -324,7 +324,7 @@ await client.deleteRow('dataset_name', rowId);
1. **禁止**在尚未获得创建授权时建表 / bind / 发布;用户明确要求「创建、完成、发布、直接做、不用询问、持续推进」均视为确认,禁止再次停下来询问
2. **禁止**让 LLM 自行发明 accessMode/拆页/口令;必须落在分支 A~E 与默认口令规则内
3. **禁止**连续两轮仅输出计划、不调用 `load_skill` / `list_dir` / `write_file` / `private_data_execute` / `bind`
4. **禁止**接受 <8 位发布口令;用户未指定口令时后台默认 **`88888888`**
4. **禁止**接受 <6 位发布口令;用户未指定口令时后台默认 **`88888888`**
5. **禁止**创建 `scripts/*-api.mjs`、Express 服务、或监听独立端口(如 `8899`
6. **禁止** HTML 中硬编码 `http://127.0.0.1:端口` 或自定义 `/api/survey/*`
7. **禁止**在 HTML 中使用 `onclick` / `oninput` 等内联事件属性(MindSpace 发布页 CSP 不允许);改用 `addEventListener`
@@ -340,7 +340,7 @@ await client.deleteRow('dataset_name', rowId);
## 交付前自检
0. 已选定分支 AE,方案摘要已确认;`password` 页口令 ≥8 位且 bind 已传入
0. 已选定分支 AE,方案摘要已确认;`password` 页口令 ≥6 位且 bind 已传入
1. `private_data_register_dataset` 已注册;PostgreSQL 表字段与 policy 一致(含 `deleted_at` 等)
2. `private_data_bind_workspace_page` **已成功**,返回 UUID 形式 `pageId``deliveryUrl`
3. `.mindspace/page-data-policies/<pageId>.json` 已写入(文件名必须是 UUID,不是 slug)
+2 -2
View File
@@ -401,12 +401,12 @@ export function MindSpacePageDataOpsPanel({ pageId, pageTitle, accessMode, onClo
type="password"
value={newPassword}
onChange={(event) => setNewPassword(event.target.value)}
placeholder="新页面口令(至少 8 位)"
placeholder="新页面口令(至少 6 位)"
autoComplete="new-password"
/>
<button
type="button"
disabled={busyAction != null || newPassword.trim().length < 8}
disabled={busyAction != null || newPassword.trim().length < 6}
onClick={() =>
void runAction('reset-password', async () => {
const result = await resetPageDataPassword(pageId, newPassword.trim());
+1 -1
View File
@@ -1362,7 +1362,7 @@ export function MindSpacePageDetail({
<input
type="password"
value={accessPassword}
minLength={8}
minLength={6}
maxLength={128}
autoComplete="new-password"
onChange={(event) => {