diff --git a/cloudfunctions/posts/index.js b/cloudfunctions/posts/index.js
index 76b3125..5096cda 100644
--- a/cloudfunctions/posts/index.js
+++ b/cloudfunctions/posts/index.js
@@ -446,6 +446,7 @@ async function listComments(event, openid, isAdmin) {
postId: post.id,
status: 'visible'
})
+ .orderBy('createdAt', 'desc')
.limit(MAX_COMMENTS_PER_POST)
.get();
const comments = (result.data || [])
diff --git a/harness/check-map-feed.mjs b/harness/check-map-feed.mjs
index 9b35206..ecb4ff9 100644
--- a/harness/check-map-feed.mjs
+++ b/harness/check-map-feed.mjs
@@ -10,6 +10,11 @@ const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
const mapJs = readFileSync(join(rootDir, 'pages/map/map.js'), 'utf8');
const mapWxml = readFileSync(join(rootDir, 'pages/map/map.wxml'), 'utf8');
const mapWxss = readFileSync(join(rootDir, 'pages/map/map.wxss'), 'utf8');
+const discoverNearbyStart = mapJs.indexOf('async discoverNearby()');
+const focusPostStart = mapJs.indexOf('\n\n focusPost(', discoverNearbyStart);
+assert.notEqual(discoverNearbyStart, -1, 'Map page should define discoverNearby.');
+assert.notEqual(focusPostStart, -1, 'Map page should define focusPost after discoverNearby.');
+const discoverNearbyBody = mapJs.slice(discoverNearbyStart, focusPostStart);
const now = Date.now();
const posts = [
{
@@ -135,5 +140,115 @@ assert.match(
/launchFocus[\s\S]*?this\.setData\(\{[\s\S]*?showList:\s*launchFocus\.showList[\s\S]*?\},\s*\(\)\s*=>\s*\{[\s\S]*?this\.applyPostFilters\(posts,\s*'all',\s*null\);[\s\S]*?this\.hideDiagnostics\(\);[\s\S]*?\}\)/,
'Focused map launches should hide startup diagnostics immediately after the list and selected task are ready.'
);
+assert.match(
+ discoverNearbyBody,
+ /async discoverNearby\(\) \{[\s\S]*?const post = nextCandidates\[Math\.floor\(Math\.random\(\) \* nextCandidates\.length\)\];[\s\S]*?selectedPost:\s*decorateMapPost\(post,\s*post\.id\),[\s\S]*?\}, \(\) => this\.refresh\(\)\);/,
+ 'Discovery should decorate only the selected post before showing the selected card.'
+);
+assert.doesNotMatch(
+ discoverNearbyBody,
+ /selectedPost:\s*post\s*[,}]/,
+ 'Discovery should not write a raw post into selectedPost while refresh is pending.'
+);
+assert.match(
+ mapJs,
+ /async buildPosts\(center\) \{[\s\S]*?return listPosts\(center,\s*\{ localOnly: true \}\);[\s\S]*?\}/,
+ 'buildPosts should keep raw derived posts and leave map decoration to list/preview builders.'
+);
+assert.doesNotMatch(
+ mapJs,
+ /buildPosts\(center\)[\s\S]*?\.map\(\(post\) => decorateMapPost\(post\)\)/,
+ 'buildPosts should not pre-decorate every post before filtering and preview building.'
+);
+assert.match(
+ mapJs,
+ /const viewportPosts = \[\];[\s\S]*?const categoryCounts = \{\};[\s\S]*?let openPostCount = 0;[\s\S]*?posts\.forEach\(\(post\) => \{[\s\S]*?if \(isOpenPost\(post\)\) \{[\s\S]*?openPostCount \+= 1;[\s\S]*?\}[\s\S]*?if \(!isPostInRegion\(post, mapRegion\)\) \{[\s\S]*?return;[\s\S]*?\}[\s\S]*?viewportPosts\.push\(post\);[\s\S]*?categoryCounts\[post\.category\]/,
+ 'Map filtering should collect viewport posts, category counts, and open-post count in a single pass.'
+);
+assert.doesNotMatch(
+ mapJs,
+ /openPostCount:\s*posts\.filter\(isOpenPost\)\.length/,
+ 'Map filtering should not rescan all posts just to compute openPostCount.'
+);
+assert.match(
+ mapJs,
+ /nearbyPreviewPosts:\s*buildNearbyPreviewPosts\(baseVisiblePosts,\s*selectedPostId\)/,
+ 'NearbyPreview should be built from raw filtered posts so map cards are not decorated twice.'
+);
+assert.doesNotMatch(
+ mapJs,
+ /nearbyPreviewPosts:\s*buildNearbyPreviewPosts\(visiblePosts,\s*selectedPostId\)/,
+ 'NearbyPreview should not re-decorate visiblePosts that were already prepared for the list.'
+);
+assert.match(
+ mapJs,
+ /this\.skipNextOnShowRefresh = true;[\s\S]*?this\.refresh\(\);[\s\S]*?onShow\(\)[\s\S]*?if \(this\.skipNextOnShowRefresh\) \{[\s\S]*?this\.skipNextOnShowRefresh = false;[\s\S]*?return;[\s\S]*?\}[\s\S]*?this\.refresh\(\);/,
+ 'The first onShow should skip the refresh already started by onLoad.'
+);
+assert.match(
+ mapJs,
+ /onHide\(\) \{[\s\S]*?this\.deactivateMapPage\(\);[\s\S]*?\}[\s\S]*?onUnload\(\) \{[\s\S]*?this\.deactivateMapPage\(\);[\s\S]*?\}[\s\S]*?deactivateMapPage\(\) \{[\s\S]*?this\.mapPageActive = false;[\s\S]*?this\.initialLocationPending = false;[\s\S]*?this\.postsRequestId = \(this\.postsRequestId \|\| 0\) \+ 1;[\s\S]*?this\.locationRequestId = \(this\.locationRequestId \|\| 0\) \+ 1;[\s\S]*?this\.discoveryRequestId = \(this\.discoveryRequestId \|\| 0\) \+ 1;[\s\S]*?this\.clearDiagnosticHideTimer\(\);[\s\S]*?\}/,
+ 'Map page should invalidate in-flight refreshes when hidden or unloaded.'
+);
+assert.match(
+ mapJs,
+ /hideDiagnosticsSoon\(\) \{[\s\S]*?if \(!this\.mapPageActive\) \{[\s\S]*?return;[\s\S]*?\}[\s\S]*?setTimeout\(\(\) => \{[\s\S]*?if \(!this\.mapPageActive\) \{[\s\S]*?this\.diagnosticHideTimer = null;[\s\S]*?return;[\s\S]*?\}/,
+ 'Map diagnostics should not schedule or run delayed setData while the page is inactive.'
+);
+assert.match(
+ mapJs,
+ /async refresh\(\) \{[\s\S]*?if \(!this\.mapPageActive\) \{[\s\S]*?return;[\s\S]*?\}[\s\S]*?const requestId = this\.nextPostsRequestId\(\);/,
+ 'Map refresh should not start while the page is inactive.'
+);
+assert.match(
+ mapJs,
+ /const posts = await this\.buildPosts\(this\.data\.center\);[\s\S]*?if \(requestId !== this\.postsRequestId\) \{[\s\S]*?return;[\s\S]*?\}/,
+ 'Map refresh should drop stale post results after awaiting buildPosts.'
+);
+assert.match(
+ mapJs,
+ /this\.locationRequestId = \(this\.locationRequestId \|\| 0\) \+ 1;[\s\S]*?const locationRequestId = this\.locationRequestId;[\s\S]*?this\.setData\(\{ center, showLocation: true, mapRegion: null \}, \(\) => \{[\s\S]*?if \(!this\.mapPageActive \|\| locationRequestId !== this\.locationRequestId\) \{[\s\S]*?return;[\s\S]*?\}[\s\S]*?this\.initialLocationPending = false;[\s\S]*?this\.refresh\(\);/,
+ 'Location setData callback should re-check page activity before refreshing.'
+);
+assert.match(
+ mapJs,
+ /showList: launchFocus\.showList,[\s\S]*?mapRegion: null[\s\S]*?\}, \(\) => \{[\s\S]*?if \(!this\.mapPageActive \|\| requestId !== this\.postsRequestId\) \{[\s\S]*?return;[\s\S]*?\}[\s\S]*?this\.applyPostFilters\(posts, 'all', null\);[\s\S]*?this\.hideDiagnostics\(\);/,
+ 'Focused launch setData callback should re-check page activity and request generation before applying filters.'
+);
+assert.match(
+ mapJs,
+ /applyPostFilters\(posts, activeCategory, mapRegion, options = \{\}\) \{[\s\S]*?if \(!this\.mapPageActive\) \{[\s\S]*?return;[\s\S]*?\}/,
+ 'Map filter application should not setData while inactive.'
+);
+assert.match(
+ mapJs,
+ /setBootStatus\(status\) \{[\s\S]*?if \(!this\.mapPageActive\) \{[\s\S]*?return;[\s\S]*?\}/,
+ 'Map boot status should not setData while inactive.'
+);
+assert.match(
+ mapJs,
+ /hideDiagnostics\(\) \{[\s\S]*?this\.clearDiagnosticHideTimer\(\);[\s\S]*?if \(!this\.mapPageActive\) \{[\s\S]*?return;[\s\S]*?\}[\s\S]*?this\.setData\(\{ diagnosticVisible: false \}\);/,
+ 'hideDiagnostics should not setData while inactive.'
+);
+assert.match(
+ mapJs,
+ /success: \(location\) => \{[\s\S]*?if \(!this\.mapPageActive \|\| locationRequestId !== this\.locationRequestId\) \{[\s\S]*?this\.initialLocationPending = false;[\s\S]*?return;[\s\S]*?\}[\s\S]*?this\.setData\(\{ center, showLocation: true, mapRegion: null \}/,
+ 'Location success should not update map data after the page becomes inactive.'
+);
+assert.match(
+ mapJs,
+ /fail: \(error\) => \{[\s\S]*?if \(!this\.mapPageActive \|\| locationRequestId !== this\.locationRequestId\) \{[\s\S]*?this\.initialLocationPending = false;[\s\S]*?return;[\s\S]*?\}[\s\S]*?addDiagnostic\('map\.getLocation\.fail'/,
+ 'Location failure should not update map diagnostics after the page becomes inactive.'
+);
+assert.match(
+ mapJs,
+ /async discoverNearby\(\) \{[\s\S]*?this\.discoveryRequestId = \(this\.discoveryRequestId \|\| 0\) \+ 1;[\s\S]*?const discoveryRequestId = this\.discoveryRequestId;[\s\S]*?await this\.discoveryCandidates\(this\.data\.activeCategory\);[\s\S]*?if \(!this\.mapPageActive \|\| discoveryRequestId !== this\.discoveryRequestId\) \{[\s\S]*?return;[\s\S]*?\}/,
+ 'Discovery should not update map state after the page becomes inactive or a newer discovery starts.'
+);
+assert.match(
+ mapJs,
+ /clearDiagnosticHideTimer\(\) \{[\s\S]*?clearTimeout\(this\.diagnosticHideTimer\);[\s\S]*?this\.diagnosticHideTimer = null;[\s\S]*?\}/,
+ 'Map diagnostic timer cleanup should cancel and reset the stored timer id.'
+);
console.log('Map feed checks passed.');
diff --git a/harness/claude-progress.md b/harness/claude-progress.md
index a36037d..5b95ffb 100644
--- a/harness/claude-progress.md
+++ b/harness/claude-progress.md
@@ -1190,3 +1190,35 @@
- 更新过的文件或工件:`pages/map/map.js`,`pages/map/map.wxml`,`harness/check-map-feed.mjs`,`harness/feature_list.json`,`harness/claude-progress.md`
- 已知风险或未解决问题:本轮只收敛“成功回首页后诊断浮层误遮挡”的问题,没有处理 DevTools 基础库偶发 `WAServiceMainContext timeout` 控制台日志;该日志此前已记录为 DevTools/native 层既有现象
- 下一步最佳动作:继续真机验证 `回首页查这条` 后列表打开、目标任务定位和无诊断遮挡;若真机仍有 native timeout 日志,再单独按 DevTools/基础库兼容问题排查
+
+### Session 086FiveRoundStabilityReview
+
+- 日期:2026-06-22
+- 工作区:`/Users/bytedance/.codex/worktrees/0422/x`;AGENTS.md 里的历史根路径是 `/Users/bytedance/git/x`,本轮按当前 Codex worktree 执行并记录
+- 本轮目标:按用户要求建立小程序开发、JS、架构三个子 agent 方向,连续至少 5 轮检查并优化性能/稳定性;每轮实现后启动同身份 reviewer,至少 2 票 PASS 才进入下一轮
+- 已完成第 1 轮:admin 身份刷新稳定性。`utils/auth.js` 的 `refreshAdminRole` 现在在无 cloud runtime、getMyRole 失败、admins 集合缺失、非 admin 或 malformed 响应时保留 guest 状态并清除 stale admin;只有 `ok === true && role === 'admin'` 才升级管理员。`scripts/check-admin-auth-errors.mjs` 扩展了缺 cloud、调用失败、集合缺失、非管理员、legacy/malformed admin、stale admin 降级和严格 admin 成功用例。复审结果:Pauli PASS、Mill PASS、Copernicus PASS
+- 已完成第 2 轮:详情页信任动作与关闭动作防重复/失败恢复。`pages/detail/detail.js`/`.wxml` 新增 `busyAction` 和 `resolving`,确认/过时/举报/关闭有 loading/disabled、try/catch/finally 和失败 toast;`utils/store.js` 在本地 fallback 中围绕异步 post lookup 做重复 reaction recheck;新增 `scripts/check-detail-action-guards.mjs` 并接入 readiness。复审结果:Einstein PASS、Schrodinger PASS、Gauss PASS
+- 已完成第 3 轮:本地关闭权限边界。`utils/store.js` 的 local `resolvePost` 只允许当前发布者或管理员关闭,抛出 `FORBIDDEN`;详情页 `canResolve` 不再信任陈旧展示态 `isMine`;新增 `scripts/check-store-permission-guards.mjs` 覆盖非 owner 拒绝、owner 成功和 admin 成功并接入 readiness。复审结果:Jason PASS、Heisenberg PASS;Lagrange 已关闭,满足两票通过
+- 已完成第 4 轮:地图页生命周期/异步竞态。`pages/map/map.js` 新增 map active 标记和 posts/location/discovery request generation,onLoad 后第一次 onShow 跳过重复 refresh,hide/unload 统一 deactivate 并清 timer;refresh、location、focused launch callback、diagnostics timer 和 discoverNearby 都会丢弃 inactive/stale 回调;`harness/check-map-feed.mjs` 增加对应静态 guard。复审结果:Parfit PASS、Lorentz PASS、Plato PASS
+- 已完成第 5 轮:云端评论 newest-first 稳定性。`cloudfunctions/posts/index.js` 的 `listComments` 现在在 `.limit(MAX_COMMENTS_PER_POST)` 前调用 `.orderBy('createdAt', 'desc')`,避免云端集合超过 50 条时先截断再 JS 排序;新增 `scripts/check-cloud-comment-order.mjs` 断言 `where -> orderBy -> limit -> get` 和 defensive sort,并接入 readiness。复审结果:Sagan PASS、Rawls PASS、Dewey PASS
+- 运行过的验证:`pwd`;读取 `harness/claude-progress.md` 与 `harness/feature_list.json`;`git log --oneline -5`;带 bundled Node PATH 的 `bash harness/init.sh`;各轮专项 `node --check` 和 `node --no-warnings` 检查;`node --no-warnings scripts/check-devtools-readiness.mjs`;子 agent 分方向审阅;最终再次运行 `node --check` 覆盖 `pages/detail/detail.js`、`pages/map/map.js`、`utils/auth.js`、`utils/store.js`、`cloudfunctions/posts/index.js`、4 个新增/扩展检查脚本、readiness 和 `harness/check-map-feed.mjs`;最终运行 `node --no-warnings` 覆盖 admin auth、detail action、store permission、cloud comment order 和 map feed guards;最终运行 `node scripts/check-json.mjs`、`node harness/check-harness.mjs`、`node --no-warnings scripts/check-devtools-readiness.mjs`、`git diff --check`、`bash harness/init.sh`
+- 已记录证据:`harness/feature_list.json` 已补充 admin/detail/map 相关 round evidence,且保留原 feature 状态;专项输出包括 `Admin auth error checks passed.`、`Detail action guard checks passed.`、`Store permission guard checks passed.`、`Cloud comment order checks passed.` 和 `Map feed checks passed.`;JSON 输出 `Checked 11 JSON files.`;harness 输出 `Harness OK: 6 features checked.`;readiness 输出最终 `DevTools readiness checks passed. Static gates passed; DevTools and real-device visual acceptance are still required.`;`git diff --check` 无输出;`bash harness/init.sh` 输出 `Harness init complete.`
+- 验证限制:尝试运行 `npm run check` 时当前 shell 返回 `zsh:1: command not found: npm`;本轮已按仓库 fallback 用 bundled Node 逐项运行等价 node 门禁,但没有得到 npm wrapper 本身的通过证据
+- 已知风险或未解决问题:没有执行真实 WeChat DevTools/真机 UI 验收;readiness 仍报告 9420 service port/smoke blocker 属于环境阻塞,不是 UI passed。第 5 轮 reviewer 提醒线上高评论量场景建议在部署/运维侧确认 `post_comments(postId, status, createdAt desc)` 这类组合索引
+- 下一步最佳动作:先完成本轮最终验证命令;随后在 WeChat DevTools/真机上人工复查 admin 角色、详情信任动作、关闭权限、地图 hide/show/定位/找附近,以及云端评论列表 newest-first 行为
+
+### Session 087FiveRoundPerformanceReview
+
+- 日期:2026-06-22
+- 工作区:`/Users/bytedance/.codex/worktrees/0422/x`;AGENTS.md 里的历史根路径是 `/Users/bytedance/git/x`,本轮按当前 Codex worktree 执行并记录
+- 本轮目标:按用户要求以性能专家身份连续进行 5 轮优化;每轮提出性能优化并启动三个同身份性能子 agent 审阅,至少 2 票 PASS 才算该轮通过
+- 已完成第 1 轮:地图 `buildPosts` 保持 raw localOnly 派生帖子,不再预先对所有帖子 `decorateMapPost`;NearbyPreview 改从 raw `baseVisiblePosts` 构建,减少重复装饰。初审 Hypatia 指出 buildPosts 仍预装饰,修复后相关后续复审通过;`discoverNearby` 同步修复为只装饰 selectedPost,避免 raw selectedPost 写入 UI
+- 已完成第 2 轮:地图 `applyPostFilters` 将屏幕内筛选、分类计数和 openPostCount 合并到一次 `posts.forEach`,减少重复全量扫描。初审 Nash/Poincare 捕获 discovery raw selectedPost 回归,修复后 Galileo、Kant、Faraday 复审 PASS
+- 已完成第 3 轮:`utils/store.js` 为 localOnly `listPosts` 增加 source-aware 短 TTL posts cache,减少地图刷新时反复读取 `wx` storage;同时用 `postsCache.source`、内部 source 派生、cloud cache 写入限制和动态 guard 防止 local/cloud/cache source 串线。多轮复审捕获并修复 localOnly/cloud 混用、cloud mutator 污染和 caller-provided source poisoning,最终 Hilbert、Peirce、Godel PASS
+- 已完成第 4 轮:`sortedDerivedPosts` 在 raw posts 阶段预过滤 hidden,避免普通列表对 hidden 帖执行 `derivePost`、距离计算和排序;`listPosts` 强制 `includeHidden: false`,`listAllPosts` 继续保留 hidden。Banach 初审指出 `listPosts(center, { includeHidden: true })` 泄漏 hidden,修复并补 guard 后 Pascal、Gibbs、Popper 复审 PASS
+- 已完成第 5 轮:`pages/admin/admin-review.js` 新增 `buildAdminSummary`,用一次遍历汇总 admin filter counts 和 stats,移除计数/统计的多次 `decoratedPosts.filter` 扫描;`scripts/check-admin-review.mjs` 补强静态 guard 并接入 readiness。Raman 初审指出 guard 未证明 summary 被使用且可漏过 hoisted repeated scans,修复后 Aristotle、Curie、Bohr 复审 PASS
+- 运行过的验证:带 bundled Node PATH 的 `bash harness/init.sh`;`node --check` 覆盖 `pages/map/map.js`、`utils/store.js`、`pages/admin/admin-review.js`、`harness/check-map-feed.mjs`、`scripts/check-performance-guards.mjs`、`scripts/check-admin-review.mjs`、`scripts/check-devtools-readiness.mjs`;`node --no-warnings harness/check-map-feed.mjs`;`node --no-warnings scripts/check-performance-guards.mjs`;`node --no-warnings scripts/check-admin-review.mjs`;`node --no-warnings scripts/check-devtools-readiness.mjs`;`node scripts/check-json.mjs`;`node harness/check-harness.mjs`;`git diff --check`
+- 已记录证据:`harness/feature_list.json` 已补充 map/admin 性能轮次证据;专项输出包括 `Map feed checks passed.`、`Performance guard checks passed.`、`Admin review helper checks passed.`;JSON 输出 `Checked 11 JSON files.`;harness 输出 `Harness OK: 6 features checked.`;readiness 输出 `DevTools readiness checks passed. Static gates passed; DevTools and real-device visual acceptance are still required.`;`git diff --check` 无输出
+- 验证限制:当前 shell 中 `command -v npm` 无输出,未运行 npm wrapper;本轮使用 bundled Node 路径逐项运行仓库门禁。没有执行真实 WeChat DevTools/真机 UI 验收,readiness 中的 9420 service-port/smoke blocker 仍是环境阻塞,不是 UI passed evidence
+- 已知风险或未解决问题:这些是性能和静态/动态 guard 优化,不改变 feature_list 的人工验收状态;地图原生层、真实定位、管理台真实管理员账号和真机性能体感仍需 DevTools/真机观察
+- 下一步最佳动作:如果要把本轮性能改动合入现有 PR,需要先提交并推送当前工作区改动;之后在 WeChat DevTools/真机复查地图列表/附近预览、localOnly 首屏刷新、hidden 任务可见性边界和管理台筛选统计
diff --git a/harness/feature_list.json b/harness/feature_list.json
index 39402b8..8f87dba 100644
--- a/harness/feature_list.json
+++ b/harness/feature_list.json
@@ -1,6 +1,6 @@
{
"project": "Street Tasks / 街区任务",
- "last_updated": "2026-06-17",
+ "last_updated": "2026-06-22",
"rules": {
"single_active_feature": true,
"passing_requires_evidence": true,
@@ -163,7 +163,13 @@
"2026-06-17: AF second review follow-up disallowed blocked conditional artifact slots when the source journey is passed, while permitting tightly justified not_applicable, and added a fieldsObserved sensitive-name denylist so coordinate, environment, identity, credential, raw payload, and raw CloudBase field names cannot be cited as safe field-only observations.",
"2026-06-17: AF third review follow-up derives conditional slot triggers from source results and environment: CloudBase enabled/deployed makes cloud-readback triggered, inspectable payload fields make payload-sample triggered, and triggered conditional slots on passed journeys must be present rather than not_applicable; snake_case identity/env aliases are now blocked in both object keys and fieldsObserved.",
"2026-06-17: AF final review follow-up added raw/cloud/file/event canonical aliases such as rawpayload, rawquery, clouduri, cloudfileid, fileid, and eventid to the shared sensitive field-name set, closing raw_payload object-key and cloud_file_id fieldsObserved bypasses; final review returned No Critical/Important findings and Ready yes.",
- "2026-06-17: AF verification covered node --check for the new artifact manifest scripts and readiness, a blocked ignored local viral results/manifest positive guard, preflight and npm run check:viral-manual-artifact-manifest positive runs, and negative cases for duplicate-key raw path leakage, missing risk-state-note, sensitive latitude key, source blocker mismatch, timeline payload missing title/imageUrl, weak CloudBase readback fields, raw camelCase payload leakage, sensitive fieldsObserved latitude/env_id, snake_case identity keys, passed source journey with blocked conditional payload slot, triggered CloudBase readback not_applicable, raw_payload object key, and cloud_file_id fieldsObserved; final npm run check and bash harness/init.sh returned 0, but the local fixture remains blocked and does not provide DevTools UI, real-device, or viral journey passed evidence."
+ "2026-06-17: AF verification covered node --check for the new artifact manifest scripts and readiness, a blocked ignored local viral results/manifest positive guard, preflight and npm run check:viral-manual-artifact-manifest positive runs, and negative cases for duplicate-key raw path leakage, missing risk-state-note, sensitive latitude key, source blocker mismatch, timeline payload missing title/imageUrl, weak CloudBase readback fields, raw camelCase payload leakage, sensitive fieldsObserved latitude/env_id, snake_case identity keys, passed source journey with blocked conditional payload slot, triggered CloudBase readback not_applicable, raw_payload object key, and cloud_file_id fieldsObserved; final npm run check and bash harness/init.sh returned 0, but the local fixture remains blocked and does not provide DevTools UI, real-device, or viral journey passed evidence.",
+ "2026-06-22: Multi-agent optimization round 4 hardened map lifecycle races: first onShow skips duplicate onLoad refresh, hide/unload deactivate invalidates posts/location/discovery request generations, in-flight refresh/location/discovery callbacks drop stale results, and diagnostic timers are cleared before inactive updates.",
+ "2026-06-22: harness/check-map-feed.mjs now guards map inactive/stale callback paths, diagnostic timer cleanup, focused launch callback guards, and discovery request invalidation; round 4 reviewers Parfit, Lorentz, and Plato all voted PASS.",
+ "2026-06-22: Performance round 1 kept pages/map buildPosts on raw localOnly posts and built NearbyPreview from baseVisiblePosts to avoid duplicate decorateMapPost work; after a reviewer caught pre-decoration and raw selectedPost edge cases, corrected guards passed re-review.",
+ "2026-06-22: Performance round 2 collapsed map viewport filtering, category counts, and open-post count into one posts.forEach pass; after fixing the discovery selectedPost decoration regression, reviewers Galileo, Kant, and Faraday voted PASS.",
+ "2026-06-22: Performance round 3 added source-aware short-TTL posts caching for localOnly listPosts to avoid repeated wx storage reads while preventing local/cloud cache pollution; after multiple reviewer-caught source and cloud-mutator fixes, reviewers Hilbert, Peirce, and Godel voted PASS.",
+ "2026-06-22: Performance round 4 prefiltered hidden raw posts before derivePost/sort while forcing normal listPosts callers to includeHidden=false and preserving listAllPosts hidden visibility; after Banach caught includeHidden leakage, reviewers Pascal, Gibbs, and Popper voted PASS."
],
"notes": "已有实现未等于 harness passing;需要手动小程序验证证据。2026-05-18 UI 刷新采用 TDesign 式设计 token 和原生小程序控件落地,未引入 tdesign-miniprogram npm 依赖,避免在当前无小程序 npm 构建链路下增加编译风险。底部 tabBar 已由悬浮胶囊改为贴底不透明导航栏。2026-05-19 发布页和地图抽屉已落地 design-ui-designer 组件化优化。2026-05-21 白屏排查未发现 WXML/WXSS/JS 语法错误,已移除 tabBar 中新增的空 cover-view 指示条以降低渲染兼容风险;同日继续排查确认 DevTools hot reload/getAppConfig 缓存会触发 subPackages 内部错误,清编译缓存并终止重编译后地图恢复,同时加入页面诊断和云读取 fallback;随后针对 `Error: timeout` 将地图首屏改为本地优先,定位、云函数列表和地图 region 读取都不再阻塞启动;详情页已补上本地 fallback,避免本地列表任务因云端不存在而显示“任务不存在”。信息列表已改为铺满抽屉、标题后置标签、底部轻量统计。A 组地图迭代新增 NearbyPreview 首屏附近优先预览和选中态联动,并收敛长标题风险;2026-06-15 用户手测暴露普通 view/button 覆盖原生 map 不可靠,因此折叠态任务入口、定位和找一找都保持为 cover-view/cover-image;经过多轮用户截图反馈,最终布局按偏好收敛为右上角白色“列表 N”单行居中按钮、右下角定位/找一找工具行、选中任务卡上移避让工具行,打开列表时仍缩小 native map 到 38vh 让普通任务卡抽屉位于 map 下方。2026-06-14 已新增地图列表静态韧性检查并接入 readiness/preflight,且手测准备 helper 会显式运行并提示该 preflight;这只降低结构、样式和执行漏跑风险,不代表 DevTools 或真机视觉验收通过。S 组补充了地图列表真实视觉 smoke 的模板 journey 和检查清单,T 组把该 journey 变成 manual evidence 必备项,U 组提供 blocked local draft helper,V 组把 blocked draft 与 sanitized summary 生成串联,W 组为 blocked JSON/summary 状态一致性增加自动 guard,X 组进一步守住 summary 与 JSON 的 branch/commit/blocker/followUp 同源完整性,Y 组把增强 guard 接入 wrapper 默认生成链路,Z 组又把生成后手工编辑必须复跑 guard 的命令直接打印到 wrapper 成功输出,AA 组新增评审前一键 preflight 扫描 ignored local blocked summaries 并逐对复跑 guard,AB 组把该 preflight 接入 harness/init.sh 和 DevTools readiness 默认检查,AC 组把 JSON/harness/readiness 门禁暴露为 npm 级 `check` 脚本,AD 组新增最小 GitHub Actions workflow 在 push/pull_request 上运行该检查,AE 组补充 required-check runbook 和稳定 job display name 以便远端 Actions/分支保护后续验证,AF 组补充 `inspect:devtools-port` 与 `check:devtools-smoke` 手动入口,明确当前 9420 service-port blocker 可复查但不进入默认 CI/check,AG 组补充 `inspect:devtools-recovery` dry-run 入口,明确恢复动作前后的 before/actions/after/next-step 证据且不执行 quit/open 副作用,AH 组补充 ignored local recovery dry-run report 生成与 guard,降低交接草稿被改写成恢复成功或 UI passed 的风险,AI 组再补充手动 preflight 扫描所有 ignored local recovery reports 并逐份复跑 guard,降低多份本地草稿交接前漏检风险;这些仍只是在证据结构上预留、守护、演练阻塞记录并提升评审可读性。viral 侧 AD/AE/AF 又把七条旅程证据包、JSON/Markdown summary 同源和附件 manifest 同源/脱敏 guard 串起来,能让真实手测后的截图/录屏/payload/readback 附件更安全地被评审引用;但这些 guard 仍不产生真实系统分享、朋友圈、payload、CloudBase readback、窄屏或真机 passed evidence。真实 safe area、地图抽屉、原生地图层、列表滚动、带图/无图和详情点击仍需 WeChat DevTools 或真机观察。仍需在 WeChat DevTools/真机中确认 safe area、键盘、地图抽屉、定位授权分支和多分类详情链路。"
},
@@ -328,7 +334,10 @@
"2026-06-17: Detail share paths created after a low-risk from=share receiver conversion now append share_id, parent_share_id, and share_depth while preserving from=share, source=receiver, and receiverAction=confirm/comment.",
"2026-06-17: Review follow-up made from=share timeline re-shares append the same relay attribution params to onShareTimeline query and record a timeline relay event; receiverConversion relay events now include conversion_action=confirm/comment derived from receiverAction.",
"2026-06-17: cloudfunctions/posts/index.js now exposes recordViralAttribution and writes sanitized events to viral_attribution_events with user_id_hash instead of raw openid; README, PROJECT_SUMMARY, and AGENTS.md document the new collection and privacy boundary.",
- "2026-06-17: scripts/check-viral-attribution.mjs is wired into scripts/check-viral-candidate.mjs and scripts/check-devtools-readiness.mjs; viral journey manual evidence now requires relay attribution params in inspectable receiver confirm/comment share payloads without claiming DevTools or real-device UI passed."
+ "2026-06-17: scripts/check-viral-attribution.mjs is wired into scripts/check-viral-candidate.mjs and scripts/check-devtools-readiness.mjs; viral journey manual evidence now requires relay attribution params in inspectable receiver confirm/comment share payloads without claiming DevTools or real-device UI passed.",
+ "2026-06-22: Multi-agent optimization round 2 added detail trust/resolve busy guards, button loading/disabled states, toastable error handling, and local duplicate trust-action rechecks around the async post lookup; scripts/check-detail-action-guards.mjs is wired into readiness and round 2 reviewers voted PASS.",
+ "2026-06-22: Multi-agent optimization round 3 moved local resolve permission enforcement into utils/store.js, allowing only owner/admin resolution and ignoring stale presentation isMine fields; scripts/check-store-permission-guards.mjs covers non-owner rejection, owner success, and admin success, and round 3 received at least two PASS votes.",
+ "2026-06-22: Multi-agent optimization round 5 moved CloudBase listComments ordering before MAX_COMMENTS_PER_POST limit with orderBy('createdAt', 'desc'), added scripts/check-cloud-comment-order.mjs, wired it into readiness, and received PASS votes from mini program, JS, and architecture reviewers."
],
"notes": "评论与信任动作都涉及本地 storage 和 CloudBase 两条路径;当前代码实现已推进,仍需要 WeChat DevTools 手动验证后才能标记 passing。2026-05-21 详情头部已补充发布者头像/名称与右侧地址时间同排展示。当前会话转向发布图片云存储修复,避免同时存在多个 in_progress 功能。 2026-06-12 C 组详情迭代新增 TrustInsight 信任判断面板和评论头部入口;尚未在 WeChat DevTools/真机验证窄屏布局、信任动作刷新、游客登录评论引导和云端评论路径。 第二轮 C 组已收敛 TrustInsight 谨慎文案和冲突优先级自动检查;2026-06-16 组合候选同时保留发布成功扩散计划和普通详情分享提示,并新增接收侧转化提示,但仍未执行 DevTools/真机视觉、分享面板、接收路径、窄屏密度、信任动作刷新和云端评论验证。 2026-06-16 评论成功后接力提示已加入组合候选:只在评论提交成功后显示,active 低风险任务可接力转发,stale/高举报/resolved/expired/hidden 不鼓励公开扩散。传播闭环候选进一步让普通分享面板在发布成功、分享接收和评论接力状态下隐藏,降低同屏传播 CTA 竞争;确认成功后接力提示进一步覆盖 confirm 高意图时刻,且 stale/report/高风险状态不鼓励公开扩散。接收者转化迭代进一步覆盖 from=share 用户完成 confirm/comment 后的二跳接力,并用 source=receiver 让下一位接收者理解链路来源;普通详情入口、stale/report/高风险和关闭态不鼓励公开扩散。本轮接收者第一步行动入口进一步让 from=share active 且无过时/举报信号的低风险用户能直接确认或补线索,并在完成 confirm/comment 后继续优先出现 receiverConversionPrompt;普通入口、有过时/举报信号、hidden/resolved/expired 不显示鼓励性 action strip。N 组新增传播链路证据框架,把首跳 from=share、接收者行动、转化提示、二跳 source=receiver 和风险态互斥固化为自动场景模型,并提供 not_run 手测模板;这仍不代表 WeChat DevTools/真机通过。仍未执行 WeChat DevTools/真机验证五种入口视觉层级、action strip 点击、open-type share、风险态按钮、窄屏换行和真实云端评论路径。 O 组进一步把 N 组 not_run 模板升级为真实本地手测结果 gate:默认只扫描 ignored/local viral journey 结果文件,无文件时通过但明确不代表 UI passed;真实文件存在时必须匹配当前分支/commit、完整环境和唯一 required journeys,并按 passed/failed/blocked 规则校验 evidence、actual、blocker、followUp、share payload 或无法检查说明以及 overallStatus 聚合。prepare helper 只生成 ignored blocked draft 或 dry-run 输出,不声称 passed。P 组新增 no-side-effect 手测启动包,把 DevTools port/smoke blocker、evidence dry-run/check 和五条 journey 下一步收敛到单一命令;当前 9420 仍是环境 blocker,不是 UI failed 或 UI passed。Q 组新增显式 blocked evidence capture 命令,把当前 port/smoke blocker 写入 ignored local JSON 并自动跑 O checker;capture 文件仍是 blocked evidence,不是 UI passed,且默认 readiness 不执行写文件 capture。R 组把接收者完成确认/评论后的二跳提示从泛化“继续接力”升级为目标化三行信息,明确推荐转给谁、为什么可信和下一位先看什么;分类文案已对齐当前 lost_found/help_needed/street_update/check_in 枚举,风险和关闭态仍不显示鼓励性 target rows。S 组进一步在低风险二跳 path 上加入 receiverAction=confirm/comment,并让下一位接收者区分上一位刚确认或刚补线索;缺失/未知 action、非 receiver source 和风险/关闭态继续安全回退。仍未执行 WeChat DevTools/真机真实链路验证。T 组在低风险 from=share 接收者完成 confirm/comment 后新增一条可转述短理由,放在目标化三行信息与继续接力按钮之间,confirm/comment 文案不同且不新增分享 query;风险、关闭和弱 stale/report 仍不显示鼓励性理由。仍未执行 WeChat DevTools/真机真实链路验证。U 组进一步在低风险 from=share 接收者完成 confirm/comment 后增加 2-3 个“适合转给”的泛化场景建议,补足 T 组 shareReason 只解决“怎么说”的短板;它不读取联系人/微信群、不新增 targeting query,风险、关闭和弱 stale/report 仍不显示鼓励性场景建议。V 组新增微信原生分享到朋友圈渠道:只在低风险 active 详情页菜单中暴露 shareTimeline,timeline query 复用 from=share 并用 source=timeline/shareChannel=timeline 区分渠道;风险、关闭、弱 stale/report 和未知任务不开放朋友圈菜单且 payload 文案谨慎。W 组把真实朋友圈渠道纳入 manual evidence schema,新增 timeline-share-channel 与 timeline-risk-gating 两条 required journey,run package 和 blocked capture 都改为七条;这仍只是证据结构与阻塞记录能力,尚未产生真实 DevTools/真机 timeline passed evidence。X 组进一步让 source=timeline 落地页不再只是普通转发语境:低风险 active 朋友圈访客会看到“朋友圈看到”的附近任务核对说明,先看状态/评论,再确认或补线索;弱 stale/report、风险和关闭态仍优先谨慎。"
},
@@ -359,7 +368,9 @@
"2026-06-13 admin iteration branch: WeChat DevTools local wcsc -lc compiled all WXSS files with exit code 0.",
"2026-06-13 admin iteration branch: bash harness/init.sh passed; npm install reported up to date, then JSON and harness checks passed.",
"2026-06-15: After user-reported getMyRole failure, added formatAdminRoleError so wx-server-sdk missing dependency and undeployed cloud function failures show short actionable admin-check messages instead of raw cloud.callFunction stack traces.",
- "2026-06-15: Added scripts/check-admin-auth-errors.mjs and wired it into scripts/check-devtools-readiness.mjs so npm run check guards admin auth error formatting."
+ "2026-06-15: Added scripts/check-admin-auth-errors.mjs and wired it into scripts/check-devtools-readiness.mjs so npm run check guards admin auth error formatting.",
+ "2026-06-22: Multi-agent optimization round 1 hardened refreshAdminRole so missing cloud runtime, getMyRole errors, missing admins collection, malformed responses, and non-admin responses preserve guest state while clearing stale admin role; only ok === true and role === 'admin' upgrades to admin. scripts/check-admin-auth-errors.mjs now covers these cases and round 1 reviewers voted PASS.",
+ "2026-06-22: Performance round 5 replaced repeated admin review filter/stat full-list scans with one-pass buildAdminSummary, kept filter/stat semantics, strengthened scripts/check-admin-review.mjs after Raman caught weak guard coverage, wired the guard into readiness, and re-reviewers Aristotle, Curie, and Bohr voted PASS."
],
"notes": "管理员身份依赖 getMyRole 云函数和本地 fallback。本实验分支已补充风险解释、建议动作、处置中防重复点击和确认弹窗信息;2026-06-15 已把云函数缺少 wx-server-sdk 或未部署的错误收敛为短提示和处理步骤。但真实管理员通过仍需要在 CloudBase 中重新上传部署 getMyRole 云函数并创建 admins 记录,且尚未在 WeChat DevTools 中以普通用户/管理员身份手动验证,因此不标记 passing。"
},
diff --git a/pages/admin/admin-review.js b/pages/admin/admin-review.js
index bc69ec9..8b8747f 100644
--- a/pages/admin/admin-review.js
+++ b/pages/admin/admin-review.js
@@ -198,8 +198,57 @@ function matchesQuery(post, query) {
return terms.every((term) => post.searchText.indexOf(term) >= 0);
}
-function countForFilter(posts, filter) {
- return posts.filter((post) => filterPost(post, filter)).length;
+function buildAdminSummary(posts) {
+ const filterCounts = adminFilterOptions.reduce((counts, item) => {
+ counts[item.value] = 0;
+ return counts;
+ }, {});
+ const stats = {
+ total: 0,
+ needsReview: 0,
+ active: 0,
+ stale: 0,
+ resolved: 0,
+ reported: 0,
+ hidden: 0
+ };
+
+ posts.forEach((post) => {
+ const postNeedsReview = needsReview(post);
+ stats.total += 1;
+ filterCounts.all += 1;
+
+ if (postNeedsReview) {
+ stats.needsReview += 1;
+ filterCounts.needs_review += 1;
+ }
+ if (post.reportCount > 0) {
+ stats.reported += 1;
+ filterCounts.reported += 1;
+ }
+ if (post.staleCount > 0 || post.status === 'stale') {
+ filterCounts.stale += 1;
+ }
+ if (post.status === 'active') {
+ stats.active += 1;
+ filterCounts.active += 1;
+ }
+ if (isClosed(post)) {
+ filterCounts.closed += 1;
+ }
+ if (post.status === 'stale') {
+ stats.stale += 1;
+ }
+ if (post.status === 'resolved') {
+ stats.resolved += 1;
+ }
+ if (post.status === 'hidden') {
+ stats.hidden += 1;
+ filterCounts.hidden += 1;
+ }
+ });
+
+ return { filterCounts, stats };
}
export function buildAdminReviewState(posts, options = {}) {
@@ -211,22 +260,15 @@ export function buildAdminReviewState(posts, options = {}) {
const visiblePosts = decoratedPosts
.filter((post) => filterPost(post, activeFilter))
.filter((post) => matchesQuery(post, query));
+ const summary = buildAdminSummary(decoratedPosts);
return {
posts: decoratedPosts,
visiblePosts,
filterOptions: adminFilterOptions.map((item) => ({
...item,
- count: countForFilter(decoratedPosts, item.value)
+ count: summary.filterCounts[item.value] || 0
})),
- stats: {
- total: decoratedPosts.length,
- needsReview: decoratedPosts.filter(needsReview).length,
- active: decoratedPosts.filter((post) => post.status === 'active').length,
- stale: decoratedPosts.filter((post) => post.status === 'stale').length,
- resolved: decoratedPosts.filter((post) => post.status === 'resolved').length,
- reported: decoratedPosts.filter((post) => post.reportCount > 0).length,
- hidden: decoratedPosts.filter((post) => post.status === 'hidden').length
- }
+ stats: summary.stats
};
}
diff --git a/pages/detail/detail.js b/pages/detail/detail.js
index ff5be97..ecca885 100644
--- a/pages/detail/detail.js
+++ b/pages/detail/detail.js
@@ -52,7 +52,7 @@ function publisherInitial(name) {
function decorateDetailPost(raw) {
const user = getCurrentUser();
const canReact = raw.status === 'active' || raw.status === 'stale';
- const canResolve = canReact && (isAdmin(user) || raw.isMine || raw.publisherId === user.id);
+ const canResolve = canReact && (isAdmin(user) || raw.publisherId === user.id);
const canShowResolve = canResolve && raw.category !== 'check_in';
const publisherName = String(raw.publisher || '附近用户').trim() || '附近用户';
return {
@@ -119,6 +119,8 @@ Page({
commentDraftLength: 0,
commentSubmitting: false,
showCommentDialog: false,
+ busyAction: '',
+ resolving: false,
maxCommentLength: MAX_COMMENT_LENGTH,
actionRelayPrompt: null,
commentRelayPrompt: null,
@@ -436,6 +438,9 @@ Page({
async react(event) {
const action = event.currentTarget.dataset.action;
+ if (this.data.busyAction) {
+ return;
+ }
if (hasReactedToPost(this.data.id, action)) {
wx.showToast({
title: '这条已记录过',
@@ -443,21 +448,35 @@ Page({
});
return;
}
- const post = await reactToPost(this.data.id, action);
- wx.showToast({
- title: action === 'report' ? '已收到举报' : '已记录',
- icon: 'success'
- });
- this.renderPost(post);
- const receiverConversionPrompt = buildReceiverConversionPrompt(post, action, {
- entryFrom: this.data.entryQuery.from
- });
- recordShareConversion(this.data.attributionSession, post, action);
- this.setData({
- actionRelayPrompt: receiverConversionPrompt ? null : buildActionRelayPrompt(post, action),
- commentRelayPrompt: null,
- receiverConversionPrompt
- });
+ this.setData({ busyAction: action });
+ try {
+ const post = await reactToPost(this.data.id, action);
+ wx.showToast({
+ title: action === 'report' ? '已收到举报' : '已记录',
+ icon: 'success'
+ });
+ this.renderPost(post);
+ if (!post) {
+ return;
+ }
+ const receiverConversionPrompt = buildReceiverConversionPrompt(post, action, {
+ entryFrom: this.data.entryQuery.from
+ });
+ recordShareConversion(this.data.attributionSession, post, action);
+ this.setData({
+ actionRelayPrompt: receiverConversionPrompt ? null : buildActionRelayPrompt(post, action),
+ commentRelayPrompt: null,
+ receiverConversionPrompt
+ });
+ } catch (error) {
+ console.warn('[detail] failed to react to post', error);
+ wx.showToast({
+ title: action === 'report' ? '举报失败,请稍后再试' : '操作失败,请稍后再试',
+ icon: 'none'
+ });
+ } finally {
+ this.setData({ busyAction: '' });
+ }
},
resolve() {
@@ -468,20 +487,38 @@ Page({
});
return;
}
+ if (this.data.resolving) {
+ return;
+ }
+ this.setData({ resolving: true });
wx.showModal({
title: this.data.post.resolveText,
content: '关闭后仍会保留在列表里,方便附近用户知道这件事已经处理完。',
confirmText: '关闭',
success: async (result) => {
if (!result.confirm) {
+ this.setData({ resolving: false });
return;
}
- const post = await resolvePost(this.data.id);
- wx.showToast({
- title: '已关闭',
- icon: 'success'
- });
- this.renderPost(post);
+ try {
+ const post = await resolvePost(this.data.id);
+ wx.showToast({
+ title: '已关闭',
+ icon: 'success'
+ });
+ this.renderPost(post);
+ } catch (error) {
+ console.warn('[detail] failed to resolve post', error);
+ wx.showToast({
+ title: error && error.code === 'FORBIDDEN' ? '没有权限关闭' : '关闭失败,请稍后再试',
+ icon: 'none'
+ });
+ } finally {
+ this.setData({ resolving: false });
+ }
+ },
+ fail: () => {
+ this.setData({ resolving: false });
}
});
},
diff --git a/pages/detail/detail.wxml b/pages/detail/detail.wxml
index d7f9502..b07405d 100644
--- a/pages/detail/detail.wxml
+++ b/pages/detail/detail.wxml
@@ -64,7 +64,7 @@
{{shareReceiverActionStrip.body}}
-
+
@@ -192,15 +192,15 @@
{{trustInsight.hint}}
-
- {{post.resolveText}}
+ {{resolving ? '关闭中' : post.resolveText}}