@microi.net/cli 6.1.8 → 6.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codebuddy-plugin/marketplace.json +2 -2
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +43 -43
- package/.mcp.json +1 -1
- package/.workbuddy-plugin/marketplace.json +2 -2
- package/.workbuddy-plugin/plugin.json +1 -1
- package/assets/build-meta.json +4 -4
- package/cordis.patch.yml +1 -1
- package/package.json +82 -82
- package/scripts/codex-marketplace.json +20 -20
- package/scripts/mcp-codex-stdio-adapter.js +189 -189
- package/scripts/mcp-server.js +100 -96
- package/scripts/mcp-trae-windows-launcher.cmd +21 -21
- package/scripts/microi-cli-mcp.js +7 -7
- package/scripts/microi-skills.meta.json +247 -235
- package/skills/.microi-skills-version.json +2 -2
- package/skills/.progressive-disclosure-manifest.json +72 -66
- package/skills/ai-engine/SKILL.md +303 -293
- package/skills/app-store/SKILL.md +2 -1
- package/skills/microi-client-frontend/SKILL.md +260 -252
- package/skills/microi-datasource-mapping/SKILL.md +4 -0
- package/skills/microi-deployment/SKILL.md +63 -5
- package/skills/microi-deployment/references/deployment-matrix.md +27 -0
- package/skills/microi-deployment/references/offline-installer.md +64 -0
- package/skills/microi-docs-coverage/references/capability-map.md +3 -3
- package/skills/microi-form-engine/SKILL.md +13 -0
- package/skills/microi-form-engine/references/component-catalog.md +1 -1
- package/skills/microi-microservice/SKILL.md +17 -0
- package/skills/microi-system-delivery/SKILL.md +8 -6
- package/skills/microi-system-delivery/references/delivery-pitfalls.md +50 -0
- package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +11 -9
- package/skills/microi-uniapp-frontend/SKILL.md +18 -3
- package/skills/module-engine/SKILL.md +4 -5
- package/skills/page-engine/SKILL.md +233 -215
- package/skills/print-engine/SKILL.md +5 -0
- package/skills/report-engine/SKILL.md +9 -0
- package/skills/report-engine/scripts/check-demo-coverage.mjs +88 -0
- package/skills/report-engine/scripts/check-demo-coverage.test.mjs +52 -0
- package/skills/scripts/sync-embedded-skills.mjs +4 -1
- package/skills/ui-design/SKILL.md +1 -1
- package/skills/uniapp-mall-assets/SKILL.md +5 -0
- package/skills/v8-file-upload/SKILL.md +18 -4
- package/skills/v8-file-upload/references/hotspot-offload-operations.md +16 -1
- package/skills/v8-saas-multi-tenant/SKILL.md +17 -4
- package/skills/v8-security/SKILL.md +20 -5
- package/skills/vision-engine/SKILL.md +114 -3
- package/skills/vision-engine/references/architecture-and-acceptance.md +8 -0
- package/skills/workspace-conventions/SKILL.md +9 -3
- package/skills/workspace-conventions/references/progressive-01-/347/211/210/346/234/254/346/233/264/346/226/260/346/227/245/345/277/227/344/277/235/346/212/244/350/247/204/345/210/231-/345/274/272/345/210/266.md +4 -4
- package/skills/workspace-conventions/references/progressive-04-/345/267/245/344/275/234/345/214/272/344/270/216/345/271/263/345/217/260/344/270/223/351/241/271/344/277/235/351/232/234.md +22 -1
|
@@ -73,6 +73,14 @@ var result = await V8.ApiEngine.Run('report_adjust_inventory', {
|
|
|
73
73
|
|
|
74
74
|
## 验收清单
|
|
75
75
|
|
|
76
|
+
### 模拟数据与当前周期(强制)
|
|
77
|
+
|
|
78
|
+
1. 先读取实际业务时间字段的最小/最大值、各期数量和状态分布,再区分无数据、日期不匹配、权限过滤、DTO/绑定错误和请求失败。数据总量足够不代表默认“本月”有数据。
|
|
79
|
+
2. 在已授权开发租户,以本次业务时区日期为 `asOf`,只补缺失的模拟订单及其主子表、履约/签收/退货/异常等适用流程。保留稳定种子标识、写前计划和实际 Id;通过正常表单事件和业务命令生成金额/库存,不直接改状态凑图表。日期时间应满足业务先后关系,不能把同日午夜承诺时间放在上午下单时间之前。
|
|
80
|
+
3. 生成可核对的 JSON `{asOf,dateField,rows,periods,stateField,requiredStates}`,运行 `node microi.skills/report-engine/scripts/check-demo-coverage.mjs <文件路径>`。`asOf` 为声明业务时区的 `YYYY-MM-DD`;`periods` 指定需有演示数据的默认周期,支持 today/week/month/quarter/year/lastYear。该检查只验证夹具覆盖,不能代替服务端授权、聚合与 UI 验收。确应为空的周期用 `allowEmptyPeriods` 显式声明并在验收报告解释,禁止为通过检查伪造交易。
|
|
81
|
+
4. 用同一日期、角色、维度从业务明细独立重算数量、金额、发运/签收和异常等指标,核对统计、分组、趋势、排行、分页明细与穿透;小数按业务精度比较。测试月末→月初、年末→年初、周一边界和明确空区间。时间区间采用约定的包含起点、不含终点语义,不能静默扩大范围掩盖空报表。
|
|
82
|
+
5. 从每个原报表菜单真实打开并截图,检查所有组件与默认筛选;月初一天的趋势允许只有一个点,切本周再验证多日数据。模拟数据仅代表演示业务,不能称为真实交易,也不默认创建持续自动造数据的定时任务。
|
|
83
|
+
|
|
76
84
|
- [ ] 指标口径、时区和小数精度明确
|
|
77
85
|
- [ ] 当前租户与行级范围在数据库查询中生效
|
|
78
86
|
- [ ] 虚拟字段与物理字段没有混用
|
|
@@ -81,3 +89,4 @@ var result = await V8.ApiEngine.Run('report_adjust_inventory', {
|
|
|
81
89
|
- [ ] 缓存按租户和权限隔离
|
|
82
90
|
- [ ] 普通角色、无权限角色、跨租户负向测试通过
|
|
83
91
|
- [ ] 配套界面引擎看板与穿透明细/导出使用同一口径,时间边界、筛选、零/空/失败及明细对账通过
|
|
92
|
+
- [ ] 模拟数据覆盖实际验收日的默认周期和关键状态,跨月/跨年复测、各组件真实数据和原菜单截图均有回执
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const dayMs = 86400000;
|
|
7
|
+
const supported = new Set(['today', 'week', 'month', 'quarter', 'year', 'lastYear']);
|
|
8
|
+
|
|
9
|
+
// 输入应先规范成业务时区;此处只比较业务日历日期,避免机器时区改变结果。
|
|
10
|
+
function calendarDay(value, dateOnly = false) {
|
|
11
|
+
if (typeof value !== 'string' || !(dateOnly
|
|
12
|
+
? /^\d{4}-\d{2}-\d{2}$/
|
|
13
|
+
: /^\d{4}-\d{2}-\d{2}(?:$|[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?$)/).test(value)) return null;
|
|
14
|
+
const parts = value.slice(0, 10).split('-').map(Number);
|
|
15
|
+
const d = new Date(0);
|
|
16
|
+
d.setUTCFullYear(parts[0], parts[1] - 1, parts[2]);
|
|
17
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
18
|
+
if (d.toISOString().slice(0, 10) !== value.slice(0, 10)) return null;
|
|
19
|
+
if (value.length > 10) {
|
|
20
|
+
const time = value.slice(11).match(/^(\d{2}):(\d{2})(?::(\d{2}))?/);
|
|
21
|
+
if (!time || Number(time[1]) > 23 || Number(time[2]) > 59 || Number(time[3] || 0) > 59) return null;
|
|
22
|
+
const offset = value.match(/[+-](\d{2}):(\d{2})$/);
|
|
23
|
+
if (offset && (Number(offset[1]) > 23 || Number(offset[2]) > 59)) return null;
|
|
24
|
+
}
|
|
25
|
+
return d.getTime();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function checkDemoCoverage(input) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return { ok: false, errors: ['Expected an input object'] };
|
|
31
|
+
const anchor = calendarDay(input.asOf, true);
|
|
32
|
+
if (anchor === null) errors.push('asOf must be a valid business calendar date YYYY-MM-DD');
|
|
33
|
+
if (typeof input.dateField !== 'string' || !input.dateField.trim()) errors.push('dateField is required');
|
|
34
|
+
if (!Array.isArray(input.rows)) errors.push('rows must be an array');
|
|
35
|
+
const periods = input.periods ?? ['today', 'week', 'month'];
|
|
36
|
+
const allowEmpty = input.allowEmptyPeriods ?? [];
|
|
37
|
+
for (const [name, values] of [['periods', periods], ['allowEmptyPeriods', allowEmpty]]) {
|
|
38
|
+
if (!Array.isArray(values) || (name === 'periods' && values.length === 0) || values.some(p => !supported.has(p))) errors.push(`${name} must contain supported period names`);
|
|
39
|
+
}
|
|
40
|
+
if (input.requiredStates !== undefined && (!Array.isArray(input.requiredStates) || input.requiredStates.some(s => typeof s !== 'string'))) errors.push('requiredStates must be an array of strings');
|
|
41
|
+
if (input.requiredStates?.length && (typeof input.stateField !== 'string' || !input.stateField.trim())) errors.push('stateField is required with requiredStates');
|
|
42
|
+
if (errors.length) return { ok: false, errors };
|
|
43
|
+
|
|
44
|
+
const d = new Date(anchor);
|
|
45
|
+
const y = d.getUTCFullYear(), m = d.getUTCMonth();
|
|
46
|
+
const make = (year, month, day = 1) => {
|
|
47
|
+
const result = new Date(0);
|
|
48
|
+
result.setUTCFullYear(year, month, day); result.setUTCHours(0, 0, 0, 0); return result.getTime();
|
|
49
|
+
};
|
|
50
|
+
const bounds = {
|
|
51
|
+
today: [anchor, anchor + dayMs],
|
|
52
|
+
week: [anchor - ((d.getUTCDay() + 6) % 7) * dayMs, anchor + dayMs],
|
|
53
|
+
month: [make(y, m), anchor + dayMs],
|
|
54
|
+
quarter: [make(y, Math.floor(m / 3) * 3), anchor + dayMs],
|
|
55
|
+
year: [make(y, 0), anchor + dayMs],
|
|
56
|
+
lastYear: [make(y - 1, 0), make(y, 0)],
|
|
57
|
+
};
|
|
58
|
+
const counts = Object.fromEntries(periods.map(p => [p, 0]));
|
|
59
|
+
const states = Object.create(null);
|
|
60
|
+
let earliest = null, latest = null;
|
|
61
|
+
input.rows.forEach((row, index) => {
|
|
62
|
+
const stamp = calendarDay(row?.[input.dateField]);
|
|
63
|
+
if (stamp === null) { errors.push(`rows[${index}].${input.dateField} is missing or invalid`); return; }
|
|
64
|
+
earliest = earliest === null ? stamp : Math.min(earliest, stamp);
|
|
65
|
+
latest = latest === null ? stamp : Math.max(latest, stamp);
|
|
66
|
+
for (const p of periods) if (stamp >= bounds[p][0] && stamp < bounds[p][1]) counts[p]++;
|
|
67
|
+
if (input.stateField) {
|
|
68
|
+
const value = row?.[input.stateField];
|
|
69
|
+
if (typeof value !== 'string' || !value) errors.push(`rows[${index}].${input.stateField} is missing or invalid`);
|
|
70
|
+
else states[value] = (states[value] || 0) + 1;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
for (const p of periods) if (counts[p] === 0 && !allowEmpty.includes(p)) errors.push(`No demo data in required period: ${p}`);
|
|
74
|
+
for (const state of input.requiredStates || []) if (!states[state]) errors.push(`No demo data in required state: ${state}`);
|
|
75
|
+
const date = stamp => stamp === null ? null : new Date(stamp).toISOString().slice(0, 10);
|
|
76
|
+
return { ok: errors.length === 0, asOf: input.asOf, totalRows: input.rows.length, minDate: date(earliest), maxDate: date(latest), periodCounts: counts, stateCounts: states, errors };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
80
|
+
try {
|
|
81
|
+
if (!process.argv[2]) throw new Error('Usage: node check-demo-coverage.mjs <input.json>');
|
|
82
|
+
const result = checkDemoCoverage(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')));
|
|
83
|
+
console.log(JSON.stringify(result, null, 2));
|
|
84
|
+
if (!result.ok) process.exitCode = 1;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(JSON.stringify({ ok: false, errors: [error.message] })); process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { checkDemoCoverage as check } from './check-demo-coverage.mjs';
|
|
9
|
+
|
|
10
|
+
const fixture = (asOf, dates, extra = {}) => ({ asOf, dateField: 'OrderDate', rows: dates.map(OrderDate => ({ OrderDate })), ...extra });
|
|
11
|
+
|
|
12
|
+
test('historic volume cannot satisfy a new default month', () => {
|
|
13
|
+
const result = check(fixture('2026-11-01', Array(120).fill('2026-10-31 09:00:00')));
|
|
14
|
+
assert.equal(result.ok, false); assert.equal(result.periodCounts.month, 0); assert.equal(result.periodCounts.week, 120);
|
|
15
|
+
});
|
|
16
|
+
test('new data fills month and each period uses its own boundary', () => {
|
|
17
|
+
const result = check(fixture('2026-11-01', ['2026-10-25', '2026-10-26', '2026-10-31', '2026-11-01', '2026-11-02']));
|
|
18
|
+
assert.equal(result.ok, true); assert.deepEqual(result.periodCounts, { today: 1, week: 3, month: 1 });
|
|
19
|
+
});
|
|
20
|
+
test('Monday and new year do not count preceding Sunday as current week', () => {
|
|
21
|
+
const result = check(fixture('2027-01-04', ['2026-12-31', '2027-01-03', '2027-01-04'], { periods: ['week', 'month', 'quarter', 'year', 'lastYear'] }));
|
|
22
|
+
assert.equal(result.ok, true); assert.deepEqual(result.periodCounts, { week: 1, month: 2, quarter: 2, year: 2, lastYear: 1 });
|
|
23
|
+
});
|
|
24
|
+
test('leap dates and midnight are valid; impossible dates/times are rejected', () => {
|
|
25
|
+
assert.equal(check(fixture('2028-02-29', ['2028-02-29T00:00:00+08:00'])).ok, true);
|
|
26
|
+
for (const date of ['2027-02-29', '2028-02-30', '2028-02-29 24:00:00', '2028-02-29T00:60:00Z', '2028-02-29T00:00:00+99:00', '']) {
|
|
27
|
+
assert.equal(check(fixture('2028-02-29', [date])).ok, false, date);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
test('explicitly legitimate empty periods remain empty', () => {
|
|
31
|
+
const result = check(fixture('2026-11-01', ['2026-11-01'], { periods: ['month', 'lastYear'], allowEmptyPeriods: ['lastYear'] }));
|
|
32
|
+
assert.equal(result.ok, true); assert.equal(result.periodCounts.lastYear, 0);
|
|
33
|
+
assert.equal(check(fixture('2026-11-01', [], { allowEmptyPeriods: ['today', 'week', 'month'] })).ok, true);
|
|
34
|
+
});
|
|
35
|
+
test('state and invalid contract checks fail closed', () => {
|
|
36
|
+
const base = fixture('2026-11-01', ['2026-11-01'], { stateField: 'Status', requiredStates: ['Draft', 'Signed'] });
|
|
37
|
+
base.rows[0].Status = 'Draft'; assert.equal(check(base).ok, false);
|
|
38
|
+
base.rows.push({ OrderDate: '2026-11-01', Status: 'Signed' }); assert.equal(check(base).ok, true);
|
|
39
|
+
for (const change of [{ periods: [] }, { periods: ['all'] }, { allowEmptyPeriods: 'month' }, { asOf: 'invalid' }, { rows: {} }, { requiredStates: 'Draft' }]) assert.equal(check({ ...base, ...change }).ok, false);
|
|
40
|
+
});
|
|
41
|
+
test('CLI exit status preserves failure and success, and teardown removes its fixture', () => {
|
|
42
|
+
const folder = mkdtempSync(path.join(tmpdir(), 'microi-report-coverage-'));
|
|
43
|
+
try {
|
|
44
|
+
const input = path.join(folder, 'input.json');
|
|
45
|
+
const cli = fileURLToPath(new URL('./check-demo-coverage.mjs', import.meta.url));
|
|
46
|
+
for (const [date, expected] of [['2026-10-31', 1], ['2026-11-01', 0]]) {
|
|
47
|
+
writeFileSync(input, JSON.stringify(fixture('2026-11-01', [date])));
|
|
48
|
+
const run = spawnSync(process.execPath, [cli, input], { encoding: 'utf8' });
|
|
49
|
+
assert.equal(run.status, expected, run.stderr); assert.equal(JSON.parse(run.stdout).ok, expected === 0);
|
|
50
|
+
}
|
|
51
|
+
} finally { rmSync(folder, { recursive: true }); }
|
|
52
|
+
});
|
|
@@ -9,7 +9,10 @@ const serverRoot = path.resolve(skillsRoot, '../Microi.Server/Microi.AI');
|
|
|
9
9
|
const project = fs.readFileSync(path.join(serverRoot, 'Microi.AI.csproj'), 'utf8');
|
|
10
10
|
const manifest = JSON.parse(fs.readFileSync(path.join(skillsRoot, '.progressive-disclosure-manifest.json'), 'utf8'));
|
|
11
11
|
const check = process.argv.includes('--check');
|
|
12
|
-
const
|
|
12
|
+
const allRegistered = process.argv.includes('--all-registered');
|
|
13
|
+
const names = allRegistered
|
|
14
|
+
? [...new Set([...project.matchAll(/Resource\\skill-([a-z0-9-]+)\.md/g)].map(match => match[1]))]
|
|
15
|
+
: process.argv.slice(2).filter(value => value !== '--check');
|
|
13
16
|
if (!names.length) throw new Error('请显式指定本次需要同步的 Skill 名称。');
|
|
14
17
|
const stripMarkers = text => text.replace(/<!--\s*\/?microi-progressive:[^>]*-->\r?\n?/g, '');
|
|
15
18
|
const digest = text => crypto.createHash('sha256').update(text).digest('hex');
|
|
@@ -216,7 +216,7 @@ Microi 项目必须支持用户或项目级形态偏好:`data-mci-shape="round
|
|
|
216
216
|
- **平台 Dialog 统一壳**:除 Drawer 外,框架弹层、微应用弹层、系统设置、商城、通知与业务表单默认使用 20–24px 大圆角、上下左右居中、标题栏可拖动的 Dialog;禁止页面自行恢复方角壳或顶部主题色粗线。标题栏左侧依次提供语义图标、英文眉题、大字标题和可选小字副标题;右上关闭按钮与标题区垂直居中并必须有可辨识的按钮底色。内容区使用连续、固定的柔和背景和整齐栅格;底部操作区右对齐大号关闭/保存按钮,两者都带语义图标。拖动与窗口缩放必须按弹层真实宽高重新约束,四边至少保留 8–18px 可视余量,不得把内容拖出视口。Teleport 到 `body` 的微应用弹层必须在弹层根同步 `data-mci-ui-root`、主题与必要 token,不能只依赖原祖先 CSS 变量。遮罩毛玻璃由 `sys_config.FormMaskBlur=1/true` 全局显式开启,缺失或 `0/false` 默认关闭;`prefers-reduced-motion` 只能停止动画与过渡,不得擅自反转静态毛玻璃配置。
|
|
217
217
|
- **Dialog 关闭按钮可见性**:右上关闭按钮固定使用 40×40px(移动端不低于 40px)中性灰底、细边框和 10–12px 圆角,不能只显示悬空的细线 X;亮色和暗色主题都必须保持可辨识背景。hover/focus-visible 时使用更深一级灰底、主题色图标和清晰焦点环。底部关闭/取消/保存按钮高度不低于 42px、最小宽度 96px,并保持右对齐与 12px 间距。
|
|
218
218
|
- **Tabs 统一视觉**:顶部工作区、数据列表 PageTabs、表单 Tabs 与右侧关联数据 Tabs 使用同一套清爽、独立标签视觉。横向 Tabs 禁止铺设贯穿整行的有色轨道、边框底板、下划线或其它装饰线;未选项保持透明,悬停与选中只使用克制的文字色、浅色独立表面和轻阴影。纵向 Tabs 可使用截图式中性浅色分组容器,选中项是独立浅色卡片。数字角标放在标签文本之后且不得挤压标题;过渡只用于交互,并尊重 `prefers-reduced-motion`。框架 `Top/TopSide` 的一级主导航属于站点导航,使用独立短底线标识当前位置,不将该样式扩散到上述 Tabs。
|
|
219
|
-
-
|
|
219
|
+
- **表格与卡片行为完整**:桌面数据卡片采用连续白色表面、弱边界和轻阴影,操作区不得再叠加有色底板或硬分割线;表格模块门头的标题/副标题卡片与每个动态统计卡片必须彼此独立,父布局保持透明,禁止用一个大卡片或外框把标题和全部统计项包在一起。DiyTable 默认保留 Element Plus 的浅色纵横网格,让列边界与调整宽度位置清晰可见;使用主题边框变量适配明暗模式,装饰分隔线不得拦截表头原生拖动命中区域。工具栏和分页不层层套框。`PageBtns`、`BatchSelect`、行内/行外 V8 按钮均不可因改版丢失;需多选而当前未选中时,应先把当前记录纳入选中集再执行。
|
|
220
220
|
- **启动状态可理解、可恢复**:Vue 挂载和业务就绪是两个状态。进度到 100% 后若仍在初始化,应显示“正在连接/后端响应较慢/仍在等待”等阶段文字;明确失败时提供原因摘要与刷新重试,禁止撤掉启动层后留下空白页。
|
|
221
221
|
- **侧栏清爽但层级清晰**:压缩无效间距,保留足够点击区域、选中态、父子层级和键盘焦点;颜色、圆角、阴影必须来自主题 token。
|
|
222
222
|
- **纵向导航使用短圆角侧标**:侧栏、纵向 Tab、分组导航等选中态的左/右侧强调线统一参照平台左侧菜单:宽 3–4px、高度约为选中项的 45%–60%,垂直居中,端点使用小圆角;禁止铺满 100% 高度,也禁止做折线或包边。右侧 Tab 镜像到右边;横向 Tabs 不使用这条侧标,也不额外增加上下指示线。
|
|
@@ -40,6 +40,7 @@ Microi 上传字段存在四种必须长期兼容的存储形态,不能假设
|
|
|
40
40
|
|
|
41
41
|
- 控件读取嵌套 `Config.ImgUpload/FileUpload.Multiple` 与 `MaxCount`;`Multiple=false` 保存单图对象,不能因数量上限或默认九图值而保存数组。已有单元素数组在单图控件中按第一项读取,不在查看时回写业务数据。
|
|
42
42
|
- 上传时透传字段授权上下文与配置的 `Limit`,后端仍独立校验字段策略。上传响应中的实际 `Limit` 属于可持久化元数据,必须保留;临时 `Url/PreviewUrl` 与本地预览地址不得入库。
|
|
43
|
+
- 内容安全审核引擎调用 `V8.Method.Upload` 时必须显式传入 `ContentSecurityRequired:true`,不能只传 `Limit:true`:字段的权威公有配置会覆盖普通 `Limit`,审核标记才会使存储策略强制使用私有桶。送审签名地址与实际存储桶必须一致,返回元数据保留实际 `Limit:true`,以便公有字段中的已审图片仍按私有方式预览。回归必须同时覆盖公有和私有字段,以及关闭审核后的普通上传;不能把微信接收送审请求等同于审核通过。
|
|
43
44
|
- 历史对象缺少 `Limit` 时,以字段配置作为取址依据。已有对象明确 `Limit=true/1` 时,即使字段当前为公有,也必须签名;私有字段上下文与实际私有标记优先于公有提示。签发失败保持空地址,禁止尝试把私有路径拼到公有 CDN。
|
|
44
45
|
|
|
45
46
|
## 统一资源解析函数
|
|
@@ -160,6 +161,10 @@ function imageUrl(path) {
|
|
|
160
161
|
- 对象 Key 必须由服务端生成并包含租户隔离前缀,不能信任客户端提交的完整路径。
|
|
161
162
|
- SVG、HTML、脚本、压缩包和办公文档按风险策略处理;可执行内容不在同源页面直接渲染。
|
|
162
163
|
- 预览失败时显示受控占位,不回退到未经校验的原始 URL。
|
|
164
|
+
- 跨端附件有持久化 `Path` 但没有 PC 队列 `State` 时,展示层按已上传兼容;明确的等待/失败状态必须保留,不为修复展示而批量改写业务记录。
|
|
165
|
+
- 小程序 `downloadFile` 得到的私有文档临时路径可能无后缀;`openDocument` 必须从原始 `Name/Path` 推导支持的 `fileType`,不从签名代理 URL 推断。覆盖 DOCX、PDF、下载失败、不支持格式和原生预览错误码回归。
|
|
166
|
+
- 未保存表单中的本地临时文档(`wxfile://`、开发者工具的 `http(s)://tmp/`)直接交给 `openDocument`,不重新下载或要求先保存业务记录。普通附件按原始名称/存储路径分流图片、视频和文档,MP4 不能误交文档预览;视频播放器可关闭并在删除/切换附件时释放。保留已保存文件的授权取址、审核拦截和单对象/多数组 JSON;回归断言预览不改业务值、不持久化临时 URL,也不回退拼接私有路径。
|
|
167
|
+
- PC 预览按附件元数据识别类型;视频支持页内播放,未配置 OnlyOffice 的 DOCX 可复用现有只读组件。不得把私有文档发送给外部在线转换服务;配置了 OnlyOffice 时保留现有授权、编辑与版本流程。
|
|
163
168
|
|
|
164
169
|
## 验收清单
|
|
165
170
|
|
|
@@ -13,7 +13,8 @@ description: Microi V8 与 MCP 文件上传下载指南。用于流式发布、
|
|
|
13
13
|
“1 GiB / 24 小时配置在哪里”时,使用已有热点分流能力。先读
|
|
14
14
|
[热点分流操作流程](references/hotspot-offload-operations.md),再核对本文的原生权限和依赖边界。
|
|
15
15
|
系统设置 `/system-config` 的 `FileServerBakThresholdGiB`、`FileServerBakWindowHours`
|
|
16
|
-
|
|
16
|
+
控制完整日流量资格;`FileServerBakSizeMiB` 控制独立单文件大小资格,需原生能力回读。
|
|
17
|
+
`FileServerBak` 是备用 HTTPS origin。可信达标后低流量不撤回 NAS 偏好。
|
|
17
18
|
只复制、保存配置或发布页面都不等于实际入口已走 NAS,也不等于完整日费用已下降。
|
|
18
19
|
|
|
19
20
|
## 源码仓库资源隔离(默认强制)
|
|
@@ -30,6 +31,19 @@ description: Microi V8 与 MCP 文件上传下载指南。用于流式发布、
|
|
|
30
31
|
|
|
31
32
|
## MCP stdio 与完整包的传输边界
|
|
32
33
|
|
|
34
|
+
### 普通本机文件上传与备用公私目标
|
|
35
|
+
|
|
36
|
+
- 先回读 `microi_get_status.PrivateFileUploadProtocol` 或 `microi_manage_file_stream_upload action=Capabilities`;支持值为 `PrivateFileUpload/1`。工具已安装不等于 API 已升级,旧 API 不支持时失败关闭,不能改走公有应用发布。
|
|
37
|
+
- `microi_upload_file_stream.localFilePath` 是 MCP 客户端电脑上的普通文件,由客户端读取原始字节、固定 16 MiB 分片;32 GiB 只是协议硬上限,实际仍受 SaaS FileUploadMaxFileMB/MaxRequestMB、DisableFileUpload 与共享日用户/租户额度约束。首次写入分片走现有 UploadQuota 原子额度,确认分片重放不重复预留,失败不退款。不要与旧 `microi_import_external_attachment.sourcePath` 混淆:后者是 API 服务账号能访问的服务器绝对路径/UNC,目标仍是主 HDFS。
|
|
38
|
+
- `target` 默认 `Primary`,`Primary` / `BackupPrivate` 均为私有;显式 `BackupPublic` 只把最终文件写入当前租户 SaaS MinIO 公桶。两种备用目标都核对 `FileServerBak` 公网绑定,暂存私桶不同于公桶且只读策略检查通过;不改 HDFS/桶策略,不允许自由 Endpoint/Bucket/凭据。只接受交互式平台管理员,禁止访问密钥会话。
|
|
39
|
+
- 只有用户明确授权选定文件可由所有人访问时才使用 `BackupPublic`,必须 `publishPublic=true`、`confirmExecution="PUBLIC:" + requestId`;私有目标确认仍是原 requestId。没有公开意图的旧调用保持私有;图库/人脸/其它私密资料不随示例公开。公开会话分片、签名清单和回执始终私有,目标和公开意图绑定签名,不能把既有私有会话换目标公开。
|
|
40
|
+
- 该入口对 MinIO 检查桶策略、OSS 检查私有 ACL 与桶策略;S3 尚缺完整私有 ACL 证明,`StorageProviderSupported=false` 时不上传。Capabilities 仅展示协议/配置,不代替实际桶权限验证。32 GiB 协议格式不放宽普通上传绝对 2 GiB 单文件/总请求保护。
|
|
41
|
+
- 稳定 `requestId` 固定原始 SHA-256/大小/每片哈希、操作者、租户及存储绑定;不确定结果先 Status 后同键续传。MCP 本机检查点不含凭据;跨客户端带原回执 `expectedStorageBindingHash`。不能取新 binding 伪装旧会话恢复。
|
|
42
|
+
- 最终返回 Completed 前服务端合并并回读原始字节 SHA-256;独立验收调用 Verify,不拿 ETag、200、元数据或上传结束代替原片完整性。会话清单/块/回执始终私有并 HMAC 绑定精确对象键,底层使用既有 HDFS 流、原生鉴权和 renewable lease,不经过 V8 大对象。
|
|
43
|
+
- 未完成会话有效期 7 天,Abort 只清理拥有的 staging;完成自动清理失败显示 CleanupComplete=false,重复 Complete/Abort 可补清。不得删除用户最终原片。签名清单/终态回执保留审计;暂存磁盘有界内存流式,最终合并需要与原片等量的临时空间。
|
|
44
|
+
- BackupPrivate 回执的 FilePathName 不能当主 HDFS 路径交给旧 GetPrivateFileUrl。没有备用私有下载票据能力时仅交付后端 Verify 证据,不能拼匿名 URL 或改公桶。当前公开热点分流仍只处理公开资源,与本协议隔离。
|
|
45
|
+
- `BackupPublic` 完成后客户端执行 `ProbePublic`:全量存储 Verify + 可信 FileServerBak 对象匿名 HEAD/首段 Range 校验,成功才返回 PublicUrl;独立 Complete 仅完成存储,没有匿名链接承诺。匿名范围最多16MiB,不冒称全文件已匿名下载;服务端全文件 SHA 与匿名可用性证据分别记录。无 Cookie/凭据/重定向,大文件错误200只读头拒绝,不下载全片。探测失败 Code0 仍保留 Completed/字节证据,沿原 requestId 或原 sessionId/binding 执行 ProbePublic 重试,不换请求号重复上传。管理 Complete 的公开确认是 `PUBLIC:` + sessionId,Abort 仍原 sessionId且只清理自有暂存。
|
|
46
|
+
|
|
33
47
|
- 标准 MCP 服务的 stdio 使用固定 `128 MiB`(`134217728` 字节)未解析 JSON-RPC 缓冲,超过边界立即拒绝并关闭连接;不读取动态环境变量放大。SDK 调用端若仍使用默认 `10 MiB` 缓冲,大响应仍可能关闭客户端,必须核对调用端支持的真实边界。
|
|
34
48
|
- 该限制计算 UTF-8 wire 字节,包含完整 JSON 信封、Base64 和重复 `Data/Result` 正文。单个 Base64 字段的理论原始字节上限约 `96 MiB` 减信封,不能据此保证任意 `64 MiB` 包响应都通过,也不能把它当成 HDFS `256 MiB` 应用包能力的新上限。
|
|
35
49
|
- 已存在的正规完整包协议仍可使用 `PreparedPersistPackageByteBase64`;先按精确正文计算编码和整个请求/响应字节数,保留版本、CAS、哈希、HDFS 回读及快照 finalize。连接关闭属于结果未知,先回读同版本、同请求身份,不能换请求键盲重传。
|
|
@@ -153,7 +167,7 @@ MinIO SDK 7 的复制签名不匹配还可能来自带参数 MIME:SDK 对源 `
|
|
|
153
167
|
可信后端 V8 可用 `V8.Http.GetResponse({ Url: url }).RawBytes` 下载,再用 `System.Convert.ToBase64String` 和 `V8.Method.Upload` 上传。该路径同样必须校验域名、大小、Content-Type、后缀和最终重定向目标。
|
|
154
168
|
|
|
155
169
|
<!-- /microi-progressive:chunk -->
|
|
156
|
-
<!-- microi-progressive:chunk id=v8-file-upload-002 sha256=
|
|
170
|
+
<!-- microi-progressive:chunk id=v8-file-upload-002 sha256=587102654ab519a0a97ffe204a0b69c06d868865f4193bfa0546365aa1fa0344 -->
|
|
157
171
|
## 接收前端上传的文件
|
|
158
172
|
|
|
159
173
|
前端发起文件上传时,平台自动把文件以 base64 形式注入到 `V8.FilesByteBase64`:
|
|
@@ -236,8 +250,8 @@ Unity `Data`、WASM、Windows 安装包、视频模型等发布资产不得进
|
|
|
236
250
|
|
|
237
251
|
用户明确授权修改某个租户的上传额度时,AI 可以直接使用标准 MCP 完成,不要把应用层提示误判成阿里云 OSS、MinIO 或 S3 的存储配额,也不要先清 Redis:
|
|
238
252
|
|
|
239
|
-
1. `
|
|
240
|
-
2.
|
|
253
|
+
1. `microi_admin_table_data(operation: "query", tableName: "sys_osclients")` 按当前 `OsClient + OsClientType + OsClientNetwork` 查询,仅选择 `Id/OsClientType/OsClientNetwork`、`DisableFileUpload` 和五个现行 `FileUpload*` 配额字段;`FileUploadEnabled` 只用于诊断未升级旧节点,不读取秘密。
|
|
254
|
+
2. 只更新上一步唯一实际生效记录,只有用户明确要求多个环境保持一致时才扩展。调用 `microi_admin_table_data(operation: "update")`,`id/row.Id` 固定已回读 Id,并传 `confirmExecution: "UPDATE:sys_osclients:<Id>"`,其余字段省略。
|
|
241
255
|
3. MB 是存储单位:`20 GB = 20480 MB`。可修改字段为 `DisableFileUpload`、`FileUploadMaxFileMB`、`FileUploadMaxRequestMB`、`FileUploadMaxCount`、`FileUploadDailyUserQuotaMB`、`FileUploadDailyTenantQuotaMB`;不要再写旧 `FileUploadEnabled`。
|
|
242
256
|
4. 保存后逐条远程回读;FormEngine 会排队重载 SaaS 运行配置,再用真实小文件上传做生效冒烟。只看到 MCP 返回“更新成功”不算验收。
|
|
243
257
|
5. 提高每日配额保留当天已用计数,剩余额度为新上限减已用量。计数按 UTC 日期,失败上传不退款;除非用户明确授权事故处置,不得删除 Redis 配额 Key。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 热点公开资源分流操作流程
|
|
2
2
|
|
|
3
|
-
<!-- microi-progressive:chunk id=v8-file-upload-011 sha256=
|
|
3
|
+
<!-- microi-progressive:chunk id=v8-file-upload-011 sha256=816ab538fe14a65f9f37aefb91dfcb0d8cbfaaa463fa7529cb7455780f96ed04 -->
|
|
4
4
|
## 从费用需求到实际分流
|
|
5
5
|
|
|
6
6
|
用户希望降低 OSS/CDN 下载费用时,先核对当前连接、运行能力、配置和真实消费入口。
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
- 系统设置路由 /system-config:备用文件服务器 FileServerBak,备用分流阈值(GiB)
|
|
12
12
|
FileServerBakThresholdGiB,备用分流统计窗口(小时)FileServerBakWindowHours。
|
|
13
|
+
- 大文件分流阈值(MiB)为 FileServerBakSizeMiB;需实际 Capabilities.SizePolicyProtocol
|
|
14
|
+
等于 PublicResourceSizeQualification/1。100 表示单文件严格超过 104857600 字节;
|
|
15
|
+
与完整日流量为或关系,空/0 关闭新大小晋级,不能把文件大小记为下载流量。
|
|
16
|
+
已确认同源偏好不随调高阈值或低流量取消;新版本、权限、健康与停用门禁不变。
|
|
17
|
+
原调度器每轮最多200条公有桶元数据、5个候选、25秒发现预算,主库签名游标续跑;
|
|
18
|
+
整轮元数据扫描完成6小时再扫,调节阈值重置游标,不重下有效副本、不创建第二任务。
|
|
19
|
+
Status.SourceObservation.LargeObjectInventory 查看进度及排除计数;列表不是对象ACL证明,
|
|
20
|
+
源头逐一公开重验、完整依赖和SHA通过后才路由。2 GiB/128对象限制与私有拒绝仍有效。
|
|
13
21
|
- 示例 1 / 24 表示一个对象或完整公开依赖组在已结束的完整北京时间自然日内,
|
|
14
22
|
可信实际下载字节达到 1073741824;不是文件大小、当前部分日或滚动窗口。
|
|
15
23
|
- 当前 FileServerBak 使用 HTTPS origin(允许端口,不带桶路径);SaaS MinIO 绑定
|
|
@@ -40,6 +48,13 @@
|
|
|
40
48
|
核对最终 URL 与 NAS 字节、MIME、CORS、HEAD/Range。静态主域仍 200 可能是保留
|
|
41
49
|
回退入口,不能仅据此推断全部失败,也不能据 NAS 副本 200 推断所有客户端已接入。
|
|
42
50
|
|
|
51
|
+
|
|
52
|
+
### 固定旧下载入口补漏
|
|
53
|
+
|
|
54
|
+
已达标且副本 Ready 不等于旧 CDN URL 已切换。对仍有下载量的硬编码地址,先核对精确对象、公有权限、来源/目标绑定、摘要、健康期和实际消费者。系统日志包 v8.6.25 的既有 mci-cdn-manager 只对白名单 static.itdos.com、file.microi.net 上三个 Chromium 137.0.7151.68 安装包支持精确重定向:ChromiumRedirectState 明确传 Domain,Preview / Submit 的 Command 明确传同一 Domain 与 Platform,沿原 RequestKey、共享域名租约和供应商 ConfigId 回执执行;结果未知只 Reconcile 原 Id。不能把它扩张为任意域名、正则或全目录替换。
|
|
55
|
+
|
|
56
|
+
两个入口都重定向时,故障回退必须离开这两个入口,使用逐个严格 TLS / HEAD 验证过的固定公共源,保留原对象 ACL;不签发私有地址。验证旧 URL → 选择接口 → NAS 的完整链、文件大小和有界 Range,同时确认其它云配置及已有规则未变。低流量不撤回偏好,健康/权限/版本失败仍可暂时回源;按每条入口实际生效时间另取完整日账单。HTTP 请求链与 HTML 相对引用证据不能冒充浏览器 Network/渲染验收。
|
|
57
|
+
|
|
43
58
|
### 请求批次与覆盖范围
|
|
44
59
|
|
|
45
60
|
前端接入读取 `microi-frontend-sdk/SKILL.md`。独立项目的 SDK 实例使用
|
|
@@ -57,6 +57,19 @@ V8.OsClientModel.AliOssPublicDomain // 可公开的文件域名
|
|
|
57
57
|
|
|
58
58
|
## 平台级配置与主租户规则
|
|
59
59
|
|
|
60
|
+
### 管理员开通的租户访问地址
|
|
61
|
+
|
|
62
|
+
`sys_osclients.DomainName` 的显示名称为“租户访问地址/域名/Url”,支持域名、
|
|
63
|
+
IP:端口及 HTTP(S) URL。同一局域网 IP 的不同端口可登记不同租户。
|
|
64
|
+
创建页从可信宿主的当前浏览器 WebBase 生成默认值:域名用 OsClient 加注册域
|
|
65
|
+
(按公共后缀表处理 com.cn/co.uk),IP 用 `{ip}:手动指定端口号`;中文提示必须
|
|
66
|
+
改为 1–65535 的实际端口,用户手动填写后不得被 OsClient 联动覆盖。不回退固定官方域。
|
|
67
|
+
|
|
68
|
+
IP、端口和完整 URL 只登记入口,不提交官方 ESA 域名绑定。仍需配置客户 Web 网关;
|
|
69
|
+
登记成功不证明可访问。前端、Managed `admin_create_empty_saas_tenant` 和
|
|
70
|
+
`SaasPromotionSecurity.TryResolveProvisioningDomain` 必须同时兼容,单发微服务不能
|
|
71
|
+
绕过旧 API 的裸域校验。公开试用继续保持独立域名为空,不扩大匿名开通权限。
|
|
72
|
+
|
|
60
73
|
### 推广中心与匿名空库开通
|
|
61
74
|
|
|
62
75
|
- 主租户现有 `V8.FormEngine` 允许显式指定子租户;子租户被 `V8TenantContext.EnforceOsClient`
|
|
@@ -130,8 +143,8 @@ V8.OsClientModel.AliOssPublicDomain // 可公开的文件域名
|
|
|
130
143
|
- 主租户由运行环境决定:优先读取环境变量 `OsClient`,其次读取 `appsettings.json` 的 `AppSettings:OsClient`。只有这条主租户 `sys_osclients` 数据中的平台级字段会作为全局配置生效。
|
|
131
144
|
- 同一个主租户可以同时运行在 `Product/Internal`、`Product/Internet` 等多个分区;租户目录、后台任务领取、通知和批量平台应用维护都必须按当前节点的 `OsClientType + OsClientNetwork` 隔离。一次全量维护只证明当前分区,事故恢复必须先盘点所有活跃分区,再从各分区对应节点分别执行并等待 `SucceededCount=ExpectedCount、FailedCount=0`,随后各自以新幂等键做零安装/零更新复跑。禁止临时改租户网络字段、跨分区借用任务或并发维护可能指向同一物理库的重复租户记录。
|
|
132
145
|
- 大范围启动事故只允许受信后台编排器使用 `MaintenanceScope=StartupDependencies`,精确闭包固定为 `app.microi.store + app.microi.saas-engine`,并可显式启用 `StartupDependencyBootstrapOnly=true`。编排器必须先用权威当前分区目录校验 `TargetOsClients`,再在子任务仍为 `Pending` 时原子写入并强回读两应用范围与只自举标志;失败即取消,禁止退化为完整安装。工作器只从官方不可变包补齐 `platform-sys-menu` 和六项 SaaS 启动门面,七项物理契约强回读通过后终止,不写安装版本。该模式只恢复可登录性,不代表 API 进程的完整运行时已经就绪;新版后端接收流量前还必须从九个内置官方基础应用包补齐并回读全部接口引擎,后续完整应用更新仍走普通商城任务。未带标志的任务行为不得改变。
|
|
133
|
-
- API 启动配置只有十项白名单:`OsClient`、`OsClientType`、`OsClientNetwork`、`OsClientDbType`、`OsClientDbConn`、`OsClientRedisHost`、`OsClientRedisPort`、`OsClientRedisPwd`、`OsClientRedisDataBase`、`OsClientDbMongoConn
|
|
134
|
-
- 存量畅捷通/微信 OAuth C# 协议网关有一组已发布到 SaaS 引擎的兼容字段:`OAuthReturnUrlOrigins`、`ChanjetOAuthState`、`ChanjetAesKey`、`ChanjetAppKey`、`WeChatTemplateAppId`、`WeChatTemplateAppSecret`、`WeChatTemplateId`、`WeChatMiniProgramAppId`。只能通过 Core 的租户绑定协议设置原子按请求权威 `OsClient` 读取,禁止使用主租户 `ConfigHelper` RuntimeConfigurationReader;整组字段不得复制给新租户,其中 OAuthState/AES Key/AppKey/AppSecret 不得进入 `V8.OsClientModel`
|
|
146
|
+
- API 启动配置只有十项白名单:`OsClient`、`OsClientType`、`OsClientNetwork`、`OsClientDbType`、`OsClientDbConn`、`OsClientRedisHost`、`OsClientRedisPort`、`OsClientRedisPwd`、`OsClientRedisDataBase`、`OsClientDbMongoConn`。除这十项外,既有部署/节点级运行参数与基础设施秘密从主控 `sys_osclients` 读取;允许各租户维护的业务配置按内容敏感性归属:开关、公开 HTTP(S) 服务地址、公开 ClientId/回调、RP/Origin/Issuer/Scope 使用该租户 `sys_config` 实体字段与普通系统设置,Secret/Token/密码/私钥、含认证信息的 URL 及确需隐藏的内部拓扑使用 `mci_system_setting`。不能仅因第三方或后端使用而私密化;公开配置仍需管理员写入权限、租户隔离、字段/地址白名单和既有 SSRF 校验。未配置时使用代码安全默认值,不因此自动迁移旧字段。官方 License 恢复次数/间隔与固定私钥挂载 `/app/microi_private.pem` 是信任链例外。禁止再增加 `MICROI_*`、`DOS_ORM_*`、自定义 `AppSettings` 节点或动态名称的环境变量读取。节点身份由平台自动生成。
|
|
147
|
+
- 存量畅捷通/微信 OAuth C# 协议网关有一组已发布到 SaaS 引擎的兼容字段:`OAuthReturnUrlOrigins`、`ChanjetOAuthState`、`ChanjetAesKey`、`ChanjetAppKey`、`WeChatTemplateAppId`、`WeChatTemplateAppSecret`、`WeChatTemplateId`、`WeChatMiniProgramAppId`。只能通过 Core 的租户绑定协议设置原子按请求权威 `OsClient` 读取,禁止使用主租户 `ConfigHelper` RuntimeConfigurationReader;整组字段不得复制给新租户,其中 OAuthState/AES Key/AppKey/AppSecret 不得进入 `V8.OsClientModel` 或前端投影且必须在审计中掩码。新集成不得继续扩展该兼容集合,由 Managed ApiEngine 编排,并按内容敏感性分别使用当前租户 `sys_config` 实体字段或 `mci_system_setting`;不批量迁移这些存量字段。
|
|
135
148
|
- `ASPNETCORE_*`、`DOTNET_*` 仅用于 .NET 宿主;构建、安装、测试、MCP、发布脚本可使用自身进程变量,但 API 生产代码不得把它们当业务配置。新增 SaaS 运行字段必须配套独立或既有 Tab、幂等升级、缓存刷新、敏感字段脱敏、子租户不继承和源码扫描测试。
|
|
136
149
|
- 文件上传的租户业务开关与额度按“当前租户 `sys_osclients` → 代码默认值”解析;现行负向字段 `DisableFileUpload` 默认关闭/空值即允许上传,只有 `1/true` 才禁止。旧 `FileUploadEnabled` 只在新物理字段尚不存在的滚动升级节点回退读取,并须在新版 SaaS 表单隐藏。平台固定灾难保护、HTTP/Multipart/Form 和反向代理上限不可由租户覆盖,也不要求安装者维护额外上传环境变量。
|
|
137
150
|
- 类似 MQTT 端口、PressureGuard、V8Limits、OrmLimits、StartupLimits、SecurityGuard 这类影响整进程资源的配置,不能让每个子租户各自抬高全局上限。子租户同名隔离字段只能降低自己的并发、等待时间或资源额度,用于隔离弱租户、试用租户或异常租户。
|
|
@@ -247,7 +260,7 @@ var secret = privateSettings['Login.Gitee.ClientSecret'];
|
|
|
247
260
|
// 禁止 return secret、console.log(secret) 或写入前端可读字段。
|
|
248
261
|
```
|
|
249
262
|
|
|
250
|
-
> `mci_system_setting`
|
|
263
|
+
> `mci_system_setting` 位于每个租户自己的数据库,保存 Secret 与确需保密的配置;仅由后端使用不是保密理由。此表普通私密值与 Secret 都不会下发浏览器;Secret 保存认证密文,只在后端 V8 的当前租户 `ServerPrivateSettings` 中解密使用。能力开关、入口显示及可公开服务地址等建成 `sys_config` 实体字段,在普通系统设置按授权维护,不能通过 `IsPublic` 或运行时勾选把旧私密记录公开。新旧配置迁移只处理明确范围,完整分类与回退规则见 `../v8-security/SKILL.md`。前端 V8、普通 FormEngine HTTP、匿名/访问密钥会话不能读取私密设置,后端 V8 也不能获得通用解密器。`sys_osclients` 自定义业务字段只保留存量兼容;共享基础设施字段由服务端强制移除,不能用自定义同义字段绕过安全代理。
|
|
251
264
|
|
|
252
265
|
## 用户扩展字段访问(同理)
|
|
253
266
|
|
|
@@ -280,7 +293,7 @@ SELECT * FROM Contact WHERE OwnerId = $CurrentUser.Id$ AND Spouse = $CurrentUser
|
|
|
280
293
|
|
|
281
294
|
- [ ] 缓存只通过 `V8.Cache` 使用逻辑 Key 或当前租户完整 Key
|
|
282
295
|
- [ ] 所有 MongoDB DbName 都包含 `V8.OsClient`
|
|
283
|
-
- [ ]
|
|
296
|
+
- [ ] 新增租户业务集成 Secret 使用 `mci_system_setting`,不再扩展 `V8.OsClientModel` 存量兼容字段;共享 Redis/存储/MQ/MQTT/Search 凭据只能由服务端托管
|
|
284
297
|
- [ ] 跨租户操作前校验权限
|
|
285
298
|
- [ ] 不向前端返回 `V8.OsClientModel`
|
|
286
299
|
- [ ] 子租户数据库账号只授权本租户库,MQ/MQTT/Search 独立凭据已真实创建
|
|
@@ -17,9 +17,9 @@ description: Microi V8 安全指南。用于审查 DiyToken 与权限、可逆
|
|
|
17
17
|
|
|
18
18
|
Microi 官方账号登录属于固定外部 Provider `Microi`,复用个人中心外部账号绑定与本租户 DiyToken。
|
|
19
19
|
先核对 `sys_config.MicroiLoginEnabled`(默认 `0`)和 `DisableLoginMicroi`(缺失/空值/`0` 显示,显式 `1` 隐藏),
|
|
20
|
-
|
|
20
|
+
再读取当前已发布入口兼容的 `Login.Microi.ClientId` 配置。该公开客户端标识历史上保存在私密 Key 中,不代表 ClientId 本身是秘密;新配置归属按下节的内容敏感性规则判断,不因修订 Skill 自动迁移已有登录配置。每个部署在官方身份服务登记独立公共客户端,
|
|
21
21
|
`ClientAuthMethod=none`、PKCE S256、nonce、精确回调;不生成、配置或分发共享 ClientSecret。
|
|
22
|
-
`Login.Microi.Scope` 默认 `openid profile email`,Name/Description
|
|
22
|
+
`Login.Microi.Scope` 默认 `openid profile email`,Name/Description 可配置;当前兼容的四项模板默认停用且由后端读取。
|
|
23
23
|
端点固定为 `https://api.microi.net/sso/iTdos/authorize`、`/token`、`/userinfo`,
|
|
24
24
|
不能从租户参数替换为任意主机。此入口使用 OAuth 2.0 授权码、PKCE S256 与官方 UserInfo,不读取或信任 `id_token`;
|
|
25
25
|
官方校验客户端、精确回调、授权码期限和 verifier,本地校验一次性 state、Bearer 类型及非空稳定 UserInfo `sub`,
|
|
@@ -57,14 +57,29 @@ Preview 和确认均重验当前客户端、回调及 Scope;账号变化通过
|
|
|
57
57
|
密码注册、重置与登录要保留相同原始输入,不能在单个环节独自 `trim`。登录及重置包装器把 `_Lang`(无值时沿用 `lang` Header,再默认 `cn`)传至认证内核;缓存未预热也应返回本地化错误而不是内部 Key。手机号作为登录别名时精确账号优先,手机号重复则拒绝;验证后的身份只按认证结果 Id 回读。账号必须 `State=1` 且未删除;历史 `State=2` 是禁用,不能为了排障绕过或自动启用。
|
|
58
58
|
|
|
59
59
|
<!-- microi-progressive:begin -->
|
|
60
|
-
<!-- microi-progressive:chunk id=v8-security-000 sha256=
|
|
60
|
+
<!-- microi-progressive:chunk id=v8-security-000 sha256=fd796e46d74942e9e8723bd8ac9a0e631bef3430120b3a942a1f5fd1ff68b05c -->
|
|
61
61
|
## 0. 租户动态系统设置与密钥边界
|
|
62
62
|
|
|
63
63
|
**AI 默认处理路径:** 用户交付第三方 Secret 并授权配置时,优先调用 `microi_manage_server_private_secret`:List 只确认 Key/HasSecret;Save 使用 `confirmExecution=SAVE:<ConfigKey>`,固定 `IsSecret=true / IsPublic=false` 并自动脱敏回读。例如畅捷通使用 `Integration.Changjet.AppSecret`,后端从 `V8.SysConfig.ServerPrivateSettings` 使用。不要仅拒绝硬编码后停止,也不要因为 `OsClientModel` 不暴露密钥而误判服务端 V8 无法调用第三方。工具不提供原文揭示;更换已有 Key 前回读现状,超时只回读、不盲目重写。
|
|
64
64
|
|
|
65
|
-
第三方密钥(微信、支付宝、OpenAI、阿里云、ERP、SMTP)**禁止**硬编码在 V8
|
|
65
|
+
第三方密钥(微信、支付宝、OpenAI、阿里云、ERP、SMTP)**禁止**硬编码在 V8 代码或前端。配置存放位置按内容敏感性判断,不以“第三方集成”或“仅后端调用”作为私密依据:可公开的租户业务配置建成当前租户 `sys_config` 的实体字段,在普通系统设置维护;Secret、凭据及确需保密的内容保存到 `mci_system_setting`。数据库、Redis、MongoDB、MinIO、MQ 等已有部署控制面仍由主库 `sys_osclients` 托管,子租户不能修改,不借本规则自动搬迁基础设施字段。
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
MinIO 部署凭据使用专用 `microi_manage_saas_minio`,不能误存到普通第三方业务 Secret,
|
|
68
|
+
也不能为此放宽 `microi_admin_table_data` 的秘密字段限制。先 Inspect 当前主租户及运行分区,
|
|
69
|
+
再以其 Revision 和 `SAVE_MINIO:<tenant>:<revision>` 确认保存;省略凭据保留,候选先探测、
|
|
70
|
+
事务行锁 CAS、提交后缓存刷新及运行回读。只返回 HasAccessKey/HasSecretKey,拒绝子租户和
|
|
71
|
+
访问密钥会话,不修改 HDFS/OSS 或桶权限。超时和 Persisted=true 的失败先 Inspect/Probe,
|
|
72
|
+
不能盲重放;旧后端不支持时升级正式后端,禁止用 SQL、普通表单或临时 V8 绕过。
|
|
73
|
+
|
|
74
|
+
| 配置内容 | 存放与维护方式 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| 能力开关、入口显示、公开交互模式、可公开的 HTTP(S) 服务地址、公开 ClientId / AppId、回调地址、RP ID、Origin、Issuer、Scope | 当前租户 `sys_config` 实体字段与普通系统设置;不能仅因供应商或使用端是后端而放入私密设置 |
|
|
77
|
+
| API Key、ClientSecret、Token、密码、签名密钥、私钥,或 URL 中的账号密码、认证查询参数/签名票据 | 当前租户 `mci_system_setting` 的 Secret;优先把公开基础地址与认证材料拆开保存,不能将整个含认证 URL 投影给浏览器 |
|
|
78
|
+
| 确有隐藏要求的内部网络拓扑、未公开管理端点或其它敏感但非凭据的值 | `mci_system_setting` 的后端私密项;说明其实际保密理由,不能只凭地址含内网 IP 或字段名作一刀切判断 |
|
|
79
|
+
|
|
80
|
+
“可公开”不等于普通用户可以修改,也不等于整条配置记录匿名下发。普通系统设置仍校验管理员/已授权角色、当前租户、字段写入白名单和审计;前端只读取经审阅的安全投影。服务地址、回调、RP/Origin/Issuer 的协议规则、精确主机/来源白名单、租户绑定及各能力既有 SSRF 校验继续生效,不能借调整存放位置放宽信任边界。
|
|
81
|
+
|
|
82
|
+
同一个新配置只保留一个写入事实源,不双写两张表。本规则不自动迁移所有旧配置,也不把已有 `mci_system_setting` 记录改为 `IsPublic`。确需迁移已授权范围内的单个配置时,先检查旧值是否含认证信息/敏感拓扑;实现新实体字段、普通设置入口、权限与投影白名单、幂等迁移和缓存回读。滚动升级可采用“新 `sys_config` 显式值 → 经核验的旧私密 Key → 存量安全默认”的只读回退,明确处理关闭/清空值;完成后从私密设置列表、保存和删除入口移除该旧 Key,不批量重写未纳入本次迁移的登录或集成配置。
|
|
68
83
|
|
|
69
84
|
```javascript
|
|
70
85
|
// ✅ 浏览器/前端 V8 只读取 sys_config 的浏览器安全投影
|
|
@@ -36,7 +36,7 @@ Microi.Vision 是可信 .NET 宿主中的最小视觉原子层,只负责:
|
|
|
36
36
|
|
|
37
37
|
| 组件 | 策略 | 职责 |
|
|
38
38
|
|---|---|---|
|
|
39
|
-
| `platform-vision-runtime` | `Managed` | Recognize/Enroll/Result/Recent/Correct/Bootstrap/Capabilities |
|
|
39
|
+
| `platform-vision-runtime` | `Managed` | Recognize/Enroll/Result/Recent/Correct/Bootstrap/Capabilities;授权视频图库 CaptureFaces/SaveCaptureSettings |
|
|
40
40
|
| `platform-vision-ai-worker` | `Managed` | 持久任务消费、Microi.AI 视觉调用、回写与敏感图片清理 |
|
|
41
41
|
| `platform-vision-category-options` | `Managed` | 当前租户启用分类选项 |
|
|
42
42
|
| `platform-vision-subject-options` | `Managed` | 当前租户启用对象选项 |
|
|
@@ -109,7 +109,7 @@ var voted = await V8.Vision.Stabilize({
|
|
|
109
109
|
## 人脸与关注人员强制边界
|
|
110
110
|
|
|
111
111
|
- 录入、识别、留存和共享生物特征前必须有合法依据、告知/同意、用途限制、访问控制和删除流程。
|
|
112
|
-
-
|
|
112
|
+
- 手工图片 Face 模式必须要求本次明确同意;视频自动图库使用管理员明确授权的租户配置,缺省关闭,不能把前端默认开关当授权。两条入口都须后端校验。
|
|
113
113
|
- 人脸模板和请求按当前租户隔离;向量使用绑定租户和接口引擎的保护能力保存,列表/Hook/普通日志不返回正文。
|
|
114
114
|
- 默认 `RetainFaceInput=0`;AI Worker 无论成功失败都清理临时图片。
|
|
115
115
|
- 数据库未命中时,大模型只能描述中性可见事实,不得猜姓名、罪犯身份、族群、健康、政治等敏感属性。
|
|
@@ -118,6 +118,105 @@ var voted = await V8.Vision.Stabilize({
|
|
|
118
118
|
|
|
119
119
|
## 视频入口
|
|
120
120
|
|
|
121
|
+
### 默认全画面与服务端识别
|
|
122
|
+
|
|
123
|
+
视频巡检必须默认全画面,无需画区或填写录像发生日期即可开启;手机摄像头打开后自动识别。
|
|
124
|
+
工位绘制采用矩形拖动或两个对角点,画面内明确提供“应用工位/重画/取消”;规则与人员核验
|
|
125
|
+
收进可选设置。历史录像未填写发生时间时,邀请时段准入保持Unknown,不使用观看时间兜底。
|
|
126
|
+
主屏必须展示工位状态、占用率/绑定员工在岗率、手机、安全帽、离岗次数/总时长/逐次区间,
|
|
127
|
+
异步行为结果标注原媒体时间,未知不能冒充正常。
|
|
128
|
+
|
|
129
|
+
v1.9.0 起禁止在巡检浏览器持续执行人员模型;手机仅采集、播放、显示节点签名结果,
|
|
130
|
+
不能再用 MediaPipe/EfficientDet 的本地框独立宣布有人。运行产物不包含推理模型/WASM。
|
|
131
|
+
手机默认后置摄像头并支持前后切换,限制720p/15fps;发送帧长边不超过1280像素,
|
|
132
|
+
检测请求串行、完成后间隔至少500ms,失败退避至10秒,画布复用。后台/卸载释放镜头,
|
|
133
|
+
暂停或切源中止检测与复核,忽略迟到结果。断流、过期帧、局部肢体和画面无效不能当无人。
|
|
134
|
+
手机大幅移动时工位统计标记中断/未知。60秒模拟负载测试不能冒充物理手机温控验收。
|
|
135
|
+
RF-DETR Large候选、YuNet/SFace和Qwen3-VL本地复核须分别锁定权重来源与许可;
|
|
136
|
+
预训练可直接推理不等于场景准确率达标,行为无正例或人脸无身份真值时不得声明90%。
|
|
137
|
+
通用节点Compose完整定义vision/behavior/media,默认启动vision+media,behavior为可选profile;API/PC继续复用,必须报告实际容器与复用原生服务的区别。
|
|
138
|
+
服务器准备入口为 `Microi.Server/tools/vision-deployment/deploy.py`,官网原位文档
|
|
139
|
+
`/doc/getting-started/docker-run.html#vision-engine-compose`。它生成一个三服务Compose,
|
|
140
|
+
要求Linux root、空目标目录、已发布镜像摘要和当前租户受限节点密钥;现有租户增设节点
|
|
141
|
+
使用 `--signing-key-file` 复用签名,不能覆盖当前租户密钥使原节点失效。模型固定下载地址、
|
|
142
|
+
大小和SHA,保留原配置及媒体,运行模型只读挂载。Ollama必须设置 `OLLAMA_NOPRUNE=true`,
|
|
143
|
+
避免启动清理未注册的已下载权重;导入阶段暂时可写且无论成功失败恢复只读。
|
|
144
|
+
官方Instruct GGUF导入明确指定 `renderer=qwen3-vl-instruct`、视觉projector和stop token,
|
|
145
|
+
使用独立模型别名,记录实际manifest digest;备份整个模型目录(包含manifests-v2)。
|
|
146
|
+
通用安装默认行为关闭;候选评估必须明确Candidate/Unvalidated,不能变正式告警。正式行为启用须有绑定摘要的独立留出集回执,不能把导入成功、JSON有效或少量样图
|
|
147
|
+
当成90%验收。服务器部署包通过官方MCP公有CDN发布并独立校验,应用私有源码与部署包分开。
|
|
148
|
+
官网必须保留完整、可复制的视觉 Compose 与服务器安装步骤;服务标题独立进入当前页大纲。
|
|
149
|
+
用户要求折叠时,代码置于默认关闭的 details/summary,标题、能力边界与配置入口仍可见;不得以折叠为由删减旧资料。群晖无宿主Python时使用
|
|
150
|
+
`microi.doc/docs/public/downloads/vision/prepare-nas.sh` 与 `import-nas.sh`,仅准备容器使用
|
|
151
|
+
CHOWN/FOWNER,不挂Docker socket;复用固定部署包、镜像与模型摘要。既有目录拒绝覆盖,
|
|
152
|
+
模型续传先校验安装标识;导入核对编排归属并恢复模型只读。DS1821+无GPU,48GB内存不能
|
|
153
|
+
作为CPU吞吐或准确率证明;Linux隔离验证、真实NAS、手机温控与独立准确率验收分别记录。
|
|
154
|
+
FP16/量化候选必须与原精度逐图比较人数、框坐标和输出有效性,出现明显漏检不得发布。
|
|
155
|
+
异步行为模型需限制图片、上下文、输出与GPU占用;超时/格式错误保持未知,不能把复核链路可调用当作动作识别达标。
|
|
156
|
+
|
|
157
|
+
录像分析优先保证采样帧与标记一致:公网中继延迟超过叠加新鲜度时,应在分析期间保留当前播放帧,收到签名结果后再继续;暂停、拖动、换源和卸载必须阻止迟到请求恢复播放。摄像头与直播不能沿用旧框制造实时识别,延迟和未知须如实呈现。验证同时检查节点推理、中继、业务保存和最终显示的总耗时,不能只报告模型 elapsedMs。
|
|
158
|
+
|
|
159
|
+
### v1.9.5 人脸定位与有界跟随
|
|
160
|
+
|
|
161
|
+
- 节点默认 YuNet 定位只产生 `faceBox/faceScore`,不因定位自动提身份模板。`evidenceKind=body/body-face/face` 区分证据:保留原人体 `box`,仅脸时 `box` 也必须是真实脸框,不补全身体。绘制优先合格脸框、否则人体;全帧 `valid=false` 保持未知清框。
|
|
162
|
+
- 摄像头显示层允许低负载灰度纹理跟随,不重新加载手机神经网络。迟到签名锚点必须按请求 FrameId/sequence 绑定原图,从原帧初始化并有界重放;禁止把旧框先套当前图再匹配。当前实现灰度长边256、8Hz、缓冲/锚点最多6秒,正反校验、纹理分数、几何与耗时预算全部通过才绘制;断流、遮挡、离开、低纹理、过期、暂停及换源及时清除。
|
|
163
|
+
- 跟随只服务显示,不生成新人数/身份/考勤证据。仅脸可证明默认全画面有人,指定工位须人体脚点;脸下缘不能当脚点。历史有效片段占用率100%与当前待判断可以并存,但UI必须显示有效观测/覆盖范围,避免被理解为当前或全片100%。
|
|
164
|
+
- 回归必须覆盖3秒以上迟到锚点重放、真实像素移动/遮挡/离开、相机媒体时钟、暂停换源晚结果、face优先/body回退、320宽和短横屏明暗布局。模拟设备与桌面纹理耗时不能冒充物理手机温控。
|
|
165
|
+
|
|
166
|
+
### 授权视频图库、示例与候选行为
|
|
167
|
+
|
|
168
|
+
- `SaveCaptureSettings` 仅当前租户管理员可用;未配置默认关闭。真正空表首次显式保存才CreateIfMissing默认策略,已有禁用/删除/非默认或歧义策略拒绝而不重置。MySQL下先锁固定sys_config行再当前读,配合ProfileKey唯一索引;不能只用进程锁或RepeatableRead旧快照宣称并发安全。
|
|
169
|
+
- `CaptureFaces` 仅接受可信节点私有签名包,绑定tenant/owner/session/sequence/time/frameId/imageSha256,且主观测valid。签名、质量、数量、私有JPEG与128维模板均有界;relay必须剥离所有私有payload。图库采用独立 `opencv-sface-2021dec-gallery-v1`,不混员工准入person Id、identityQuality或subjectId。
|
|
170
|
+
- 对象/样本通过runtime保护模板和私有存储入库;未知建未命名人员,模糊区不乱合并也不反复建对象,后续名称取权威subject.Name。每帧最多新增1张、公平轮转,每人最多3张且间隔至少30秒;256帧有界ledger记录零新增与成功回执,同帧改包拒绝。当前读与真实双并发验收须在实际提供商执行;当前自动归档仅MySQL验证,其它提供商在上传前失败关闭。
|
|
171
|
+
- Capture使用独立事务;失败仅回captureError,不能阻断已验签人员观测。失败上传只能补偿本次自有私有路径;数据库事务不等于对象存储原子回滚。普通结果仅gallerySubjectId/gallerySubjectName/samplesAdded,不返回图片、模板或私有路径;runtime/monitor/relay关闭请求正文日志。
|
|
172
|
+
- Bootstrap的CanCaptureFaces来自当前租户授权与管理员权限,前端开关仅在true时有效。ExampleVideos只返回管理员明确配置、可播放的本租户私有列表;桌面显著提供示例入口,手机初始仍camera。官方包DataSelections只保留非空分类种子,不导出profile/AutoCaptureFaces/ExampleVideosJson/CaptureReceiptsJson及任何客户样本或录像。
|
|
173
|
+
- 行为使用POST `/behavior-jobs`、GET/DELETE `/behavior-jobs/{32位小写hex}`,单并发、2秒轮询、有界600秒生命周期;暂停/切源/卸载取消所属旧节点任务。Queued/Running/Completed/Failed/Cancelled/Expired分别呈现。Completed必须核对原session/sequence/time与签名,来源错配拒绝。
|
|
174
|
+
- 安全帽/手机使用/拍摄当前进入candidateEvents,标Candidate/Unvalidated/需复核、原帧时间和模型摘要,formalBehaviorCount保持0;不能靠节点自报validated升级正式违规。完成无候选也不构成90%通过;模型未配不发无效任务。正式门禁、候选演示与专用模型研究分开报告。
|
|
175
|
+
- UI以视频、紧凑工具栏与六指标为核心,历史/候选明细用抽屉或内部滚动,不堆叠长页。明暗/安全区/320宽/手机前后切换继续验收;不为科幻效果牺牲未知提示或添加高耗持续动画。
|
|
176
|
+
|
|
177
|
+
工作台提供可见的服务设置入口,通过 capabilities 区分节点未连接、模型未配置、复核失败与画面未知;缺少行为模型时不发送无效 review,也不显示一直“复核中”。NAS 平台中继无需浏览器填写 NAS 内网地址;直连的公开地址由管理员在普通系统设置的 sys_config.VisionEdgePublicBase 维护,签名密钥 Vision.Edge.SigningKey 才属于安全与服务接入,不得暴露或被页面覆盖。公开地址仍须经过租户与可信来源校验。专用 CPU 模型诊断、目标 NAS 吞吐和独立场景准确率分别验收。
|
|
178
|
+
|
|
179
|
+
用户指定DSM反向代理时按实际规则核查HTTPS域名/证书、目标节点环回端口和空闲监听端口;指定1000–1999范围也不能猜端口或覆盖既有规则。配置成功后将公开HTTPS地址经同源MCP保存普通VisionEdgePublicBase并回读,再验收直连与允许来源;秘密仍仅在安全配置。反代、应用发布、节点运行及物理手机分别留证,不以中继可用替代用户指定的直连交付。
|
|
180
|
+
|
|
181
|
+
`OfficialApplications/Vision` 是应用工程,`src`为Vue微服务、`engines`为V8业务,
|
|
182
|
+
`edge`为额外部署的Python视频节点;`Microi.Vision`仍是独立闭源.NET内核。
|
|
183
|
+
应用源码发布不能冒充.NET内核更新或节点部署;不能因目录都包含Vision而声称后端完全统一。
|
|
184
|
+
|
|
185
|
+
### 视频巡检模式(v1.5)
|
|
186
|
+
|
|
187
|
+
`microi-vision/workbench` 保留图片模式,增加录像/手机摄像头/登记监控源巡检。
|
|
188
|
+
责任源码为 `Microi.Server/OfficialApplications/Vision`,规范由
|
|
189
|
+
`OfficialApplications/Resource/vision-release.json` 锁定;旧 AI-Project 镜像不作为新版本发布源。
|
|
190
|
+
|
|
191
|
+
- 两张新增表 `mci_vision_monitor_session`、`mci_vision_person` 与 Managed
|
|
192
|
+
`platform-vision-monitor` 通过 MCP Manifest 升级;不新增 .NET 定制迁移或业务 Controller。
|
|
193
|
+
- `Create/Read/Close/Bootstrap/Process/Review` 管理本人会话;
|
|
194
|
+
`People/SavePerson/ClearFace/EnrollFace/MatchFaces` 管理准入与受保护模板。
|
|
195
|
+
- 受信边缘节点只解码/转码/推理,DiyToken 向当前租户核验身份;媒体票据短期有效。
|
|
196
|
+
RTSP 只来自节点登记的 MediaMTX 路径,摄像头凭据不进入浏览器、普通 V8 或日志。
|
|
197
|
+
- 检测帧 HMAC 绑定 owner、tenant、session、sequence、媒体时刻和新鲜度;
|
|
198
|
+
`Process` 以数据库 Revision CAS 保存,重复/乱序不重复积分。
|
|
199
|
+
签名使用真实原子 `V8.Method.HmacSha256Sign(observation, key)`,返回 Base64。
|
|
200
|
+
节点密钥在 `V8.SysConfig.ServerPrivateSettings['Vision.Edge.SigningKey']`,通过
|
|
201
|
+
`microi_manage_server_private_secret` 配置,禁止进入源码。
|
|
202
|
+
- `Review` 只追加原视频时刻的候选行为事件,不重放考勤积分、不替换当前检测框,也不计正式违规。
|
|
203
|
+
一个节点同时最多一项行为复核;人员检测保持独立,不等待大模型完成。
|
|
204
|
+
- 工位不绑定人员时叫“工位占用率”;绑定后才叫“员工在岗率”。
|
|
205
|
+
身份不明不能替指定员工算在岗,跳播/断流/无效帧前后间隔为未知。
|
|
206
|
+
录像覆盖率用全片时长作分母,观测区间取并集;倒退识别必须新开会话,不能重复累计同一段离岗。
|
|
207
|
+
- 历史录像的 `rules.recordedAt` 与媒体时间决定邀请核验时刻;签名新鲜度仍由 wallTimeMs 决定。
|
|
208
|
+
- YuNet/SFace 等实际人脸检测和五点对齐后才生成模板;宽高不足80像素、模糊、
|
|
209
|
+
多人录入不合格。合格未匹配人脸只能产生未登记复核线索,不直接驱动门禁或处罚。
|
|
210
|
+
- 人员/手机/安全帽/拍摄分别标注评估;无正例、未知和模拟视频不能证明90%识别率。
|
|
211
|
+
具体边缘协议与验收见责任源码 `edge/README.md` 和
|
|
212
|
+
[架构与验收](references/architecture-and-acceptance.md)。
|
|
213
|
+
|
|
214
|
+
已有通用 MCP 的 `microi_get_db_schema`、`microi_plan_system`、
|
|
215
|
+
`microi_generate_system`、`microi_validate_system`、`microi_save_engine_code`、
|
|
216
|
+
`microi_run_engine`、私有源码同步和 V3 发布工具已覆盖这条链路;无需增加绕过权限的摄像头工具。
|
|
217
|
+
`platform-vision-ai-worker` 延迟重试用 `await System.Threading.Tasks.Task.Delay(ms)`,
|
|
218
|
+
不能调用不存在的 `V8.Action.Sleep`。
|
|
219
|
+
|
|
121
220
|
前端使用 `getUserMedia` 并按 `FrameIntervalMs` 取帧,必须提供开启、暂停、继续、立即识别当前帧和关闭操作;每次打开摄像头生成新的 `StreamSessionId`,每帧递增 `FrameSequence`。自动连续模式允许跳过相同画面,人工主按钮必须显式绕过前端去重以提交当前帧。可信边缘宿主将 RTSP/WebRTC 解码为 `IAsyncEnumerable<MicroiVisionExtractParam>` 后调用 `RecognizeFramesAsync`。Microi.Vision 不直接连接调用方提交的 RTSP URL,不保存摄像头账号,不承担流媒体服务器职责。
|
|
122
221
|
|
|
123
222
|
打开摄像头、`getUserMedia`、`video.play()`、关闭和组件卸载之间必须使用递增操作令牌或等价取消机制。关闭时先同步写入关闭态并使旧操作失效,再停止轨道和清空 `srcObject`;不能等待媒体清理完成后才改 UI,否则快速关闭会被较早的异步打开流程反向覆盖。移动控件或条件渲染按钮可在 `pointerdown` 立即执行停止语义,并对随后 `click` 做短时去重,防止 DOM 重排吞掉关闭操作。
|
|
@@ -132,10 +231,18 @@ var voted = await V8.Vision.Stabilize({
|
|
|
132
231
|
|
|
133
232
|
## 交付顺序
|
|
134
233
|
|
|
234
|
+
### 视频巡检的手机连接与准确率门禁
|
|
235
|
+
|
|
236
|
+
手机默认选择平台HTTPS中继(`edge=platform`),避免手机把电脑的环回节点当作自身访问。复用 `platform-vision-edge-relay` / `platform-vision-edge-media` 与16个固定共享技术槽位,节点仅出站连接;受限访问密钥兑换DiyToken,Windows凭据用CurrentUser DPAPI保护。原始图片/特征仅进入短期加密缓存或节点有界内存,不记录在槽位表、浏览器结果或普通日志;官方包不包含节点凭据、运行槽位数据和客户录像。仍支持已登记HTTPS节点直连。
|
|
237
|
+
|
|
238
|
+
手机视频使用页面内控制和大画面保留跟随标注;不要调用iOS原生video全屏来承载SVG。画区时收起规则面板、定位取景区,控件至少44px,处理自动播放拒绝、横竖屏、相机关闭竞态。录像从节点原分辨率提帧,私有HLS只承担播放;分片上传每段SHA-256,不将整个长视频Base64放入V8。
|
|
239
|
+
|
|
240
|
+
90%以上必须由真实独立留出集分能力证明。应用准确率门禁要求各项准确率/精确率/召回率/特异度/覆盖率>90%,未知保留在分母;缺少正例或身份真值时不能声明达标。离岗区间另做时间线对账。完整边界与MCP/浏览器验收见 [architecture-and-acceptance.md](references/architecture-and-acceptance.md)。
|
|
241
|
+
|
|
135
242
|
1. 更新并测试 Microi.Vision、V8 注入与 API DI;
|
|
136
243
|
2. 用 MCP `microi_plan_system` 做 dry run,再生成并 `microi_validate_system`;
|
|
137
244
|
3. 同步私有微服务源码,构建后按 V3 stage/finalize 发布公开产物并回读;
|
|
138
|
-
4.
|
|
245
|
+
4. 只发布默认分类种子;策略由管理员显式保存时安全初始化,禁止导出或覆盖租户授权、示例与回执;
|
|
139
246
|
5. 发布框架 NuGet/Docker,使目标运行时实际包含 `V8.Vision`;
|
|
140
247
|
6. 通过官方应用发布引擎生成商城包,明确 `Managed/CreateIfMissing`,回读包 SHA 和不可变版本;
|
|
141
248
|
7. 在目标租户安装/更新后做真实浏览器、接口、后台任务和模型测试;
|
|
@@ -157,4 +264,8 @@ var voted = await V8.Vision.Stabilize({
|
|
|
157
264
|
|
|
158
265
|
## 验收输出
|
|
159
266
|
|
|
267
|
+
视频工作台保持画面优先:紧凑模式栏和来源/识别操作,六项核心指标可见,说明与明细按需展开。
|
|
268
|
+
覆盖暂停、切源、卸载与自动播放受限的异步播放竞态;正常 AbortError 不冒充故障,真实媒体错误不能静默吞掉。
|
|
269
|
+
Python 节点迁入 .NET 时必须分别列明媒体网关、FFmpeg、模型/GPU、行为模型资源及差分验证;未实际迁移不得声称已无额外运行依赖。
|
|
270
|
+
|
|
160
271
|
最终报告至少分开列出:源码测试、5 表/字段/索引、7 菜单、5 引擎/4 事件、微服务源码与构建哈希、商城包与版本、框架制品、目标节点版本、真实浏览器、PC/手机横竖屏布局、历史图片与纠错、真实图片素材来源、数据库命中、AI 回退、生鲜属性/价格来源、人脸同意、模型准确率和未执行的物理设备测试。
|