Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 884e819f70 |
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 是否生成且列白名单与表单字段一致',
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,7 +46,7 @@ HTML 页面只能调用 Page Data API(page-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 API(page-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 API(page-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 API(page-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 API(page-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. 已选定分支 A~E,方案摘要已确认;`password` 页口令 ≥8 位且 bind 已传入
|
||||
0. 已选定分支 A~E,方案摘要已确认;`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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -1362,7 +1362,7 @@ export function MindSpacePageDetail({
|
||||
<input
|
||||
type="password"
|
||||
value={accessPassword}
|
||||
minLength={8}
|
||||
minLength={6}
|
||||
maxLength={128}
|
||||
autoComplete="new-password"
|
||||
onChange={(event) => {
|
||||
|
||||
Reference in New Issue
Block a user