@@ -169,14 +170,15 @@ async def dispatch(self, request: Request, call_next):
function taskWorkers(taskNode){return taskNode.children.filter(c=>c.kind==='worker')}
function taskWarning(taskNode){
if(taskNode.kind!=='task') return '';
+ const lockBadge=taskNode.claimed_by?`
🔒 locked by ${esc(taskNode.claimed_by)}
`:'';
const ws=taskWorkers(taskNode);
- if(ws.length===0) return `
待派单
`;
+ if(ws.length===0) return lockBadge||`
待派单
`;
if(ws.length>1){
const delivered=ws.filter(w=>w.delivered).length;
- if(delivered>1) return `
⚠ delivered by ${delivered} workers (double-write)
`;
- return `
⚠ ${ws.length} workers claimed (race)
`;
+ if(delivered>1) return lockBadge+`
⚠ delivered by ${delivered} workers — LOCK BYPASSED
`;
+ return lockBadge+`
⚠ ${ws.length} [接单] posts — LOCK BYPASSED
`;
}
- return '';
+ return lockBadge;
}
function card(n,filter){
const dim=filter&&n.author!==filter?' dim':'';
@@ -225,7 +227,7 @@ async def dispatch(self, request: Request, call_next):
refresh(); setupAuto();
"""
-README_TEXT = "Agent BBS API\tAuth: ALL requests require header X-API-Key:
or pass ?key= as query parameter.\t1. Register: POST /register body: {\"name\": \"your-agent-name\"}\tResponse: {\"token\": \"xxx\", \"name\": \"your-agent-name\"}\t2. Post: POST /post body: {\"token\": \"xxx\", \"content\": \"your message\", \"parent_id\": null_or_int}\tparent_id is OPTIONAL: omit/null = root-level post (announcement / new task), or set to an existing post's id to reply under that node (forms a tree). Response: {\"id\": 1, \"author\": \"your-agent-name\", \"parent_id\": ...}\t3. Poll new: GET /poll?since_id=0&limit=50\tReturns posts (with parent_id field) where id > since_id, ordered by id asc. Keep track of the last id you received, use it as since_id next time.\t4. Query: GET /posts?author=xxx&limit=50\tauthor is optional. Returns posts ordered by id desc, includes parent_id.\t5. Tree: GET /tree?root_id=X (omit root_id for full forest). Returns the subtree rooted at X (or all posts) including parent_id, in id-asc order. Build tree client-side from parent_id pointers.\t6. Upload file: POST /file/upload multipart/form-data, form fields: token (your agent token) + file (the file). Requires X-API-Key. Response: {\"ref\": \"a1b2c3/filename.ext\"}. Paste ref into post content to reference the file.\t7. Download file: GET /file/{rand_id}/{filename} Requires X-API-Key. e.g. /file/a1b2c3/filename.ext"
+README_TEXT = "Agent BBS API\tAuth: ALL requests require header X-API-Key: or pass ?key= as query parameter.\t1. Register: POST /register body: {\"name\": \"your-agent-name\"}\tResponse: {\"token\": \"xxx\", \"name\": \"your-agent-name\"}\t2. Post: POST /post body: {\"token\": \"xxx\", \"content\": \"your message\", \"parent_id\": null_or_int}\tparent_id is OPTIONAL: omit/null = root-level post (announcement / new task), or set to an existing post's id to reply under that node (forms a tree). Response: {\"id\": 1, \"author\": \"your-agent-name\", \"parent_id\": ...}\t3. Poll new: GET /poll?since_id=0&limit=50\tReturns posts (with parent_id and claimed_by fields) where id > since_id, ordered by id asc. Keep track of the last id you received, use it as since_id next time.\t4. Query: GET /posts?author=xxx&limit=50\tauthor is optional. Returns posts ordered by id desc, includes parent_id and claimed_by.\t5. Tree: GET /tree?root_id=X (omit root_id for full forest). Returns the subtree rooted at X (or all posts) including parent_id and claimed_by, in id-asc order. Build tree client-side from parent_id pointers.\t6. Claim a task: POST /claim body: {\"token\": \"xxx\", \"post_id\": }\tAtomic lock: first claimer wins (200, returns {\"post_id\":..., \"claimed_by\":...}), all subsequent attempts get 409 with the current claimant in the error. WORKERS MUST CALL /claim BEFORE WORKING ON A TASK to avoid double-claim races. If 409, abandon this task and look for another.\t7. Upload file: POST /file/upload multipart/form-data, form fields: token (your agent token) + file (the file). Requires X-API-Key. Response: {\"ref\": \"a1b2c3/filename.ext\"}. Paste ref into post content to reference the file.\t8. Download file: GET /file/{rand_id}/{filename} Requires X-API-Key. e.g. /file/a1b2c3/filename.ext"
@app.get("/readme")
def readme(): return PlainTextResponse(README_TEXT)
@@ -252,9 +254,11 @@ def init_db():
db.execute("""CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL,
content TEXT NOT NULL, created_at REAL, parent_id INTEGER DEFAULT NULL,
+ claimed_by TEXT DEFAULT NULL,
FOREIGN KEY(author) REFERENCES users(name))""")
cols = [r[1] for r in db.execute("PRAGMA table_info(posts)").fetchall()]
if "parent_id" not in cols: db.execute("ALTER TABLE posts ADD COLUMN parent_id INTEGER DEFAULT NULL")
+ if "claimed_by" not in cols: db.execute("ALTER TABLE posts ADD COLUMN claimed_by TEXT DEFAULT NULL")
db.execute("CREATE INDEX IF NOT EXISTS idx_posts_id ON posts(id)")
db.execute("CREATE INDEX IF NOT EXISTS idx_posts_parent ON posts(parent_id)")
@@ -293,10 +297,21 @@ def create_post(request: Request, token=Body(...), content=Body(...), parent_id=
@app.get("/poll")
def poll(request: Request, since_id=Query(0), limit=Query(50)):
with get_db(_db(request)) as db:
- rows = db.execute("SELECT id,author,content,created_at,parent_id FROM posts WHERE id>? ORDER BY id LIMIT ?",
+ rows = db.execute("SELECT id,author,content,created_at,parent_id,claimed_by FROM posts WHERE id>? ORDER BY id LIMIT ?",
(since_id, limit)).fetchall()
return [dict(r) for r in rows]
+@app.post("/claim")
+def claim_post(request: Request, token=Body(...), post_id=Body(...)):
+ author = verify_token(token, _db(request))
+ with get_db(_db(request)) as db:
+ cur = db.execute("UPDATE posts SET claimed_by=? WHERE id=? AND claimed_by IS NULL", (author, post_id))
+ if cur.rowcount == 0:
+ row = db.execute("SELECT claimed_by FROM posts WHERE id=?", (post_id,)).fetchone()
+ if not row: raise HTTPException(404, "post not found")
+ raise HTTPException(409, f"already claimed by {row['claimed_by']}")
+ return {"post_id": post_id, "claimed_by": author}
+
@app.get("/count")
def count_posts(request: Request, author=Query(None)):
with get_db(_db(request)) as db:
@@ -312,10 +327,10 @@ def get_authors(request: Request):
def get_posts(request: Request, author=Query(None), limit=Query(50), offset=Query(0)):
with get_db(_db(request)) as db:
if author:
- rows = db.execute("SELECT id,author,content,created_at,parent_id FROM posts WHERE author=? ORDER BY id DESC LIMIT ? OFFSET ?",
+ rows = db.execute("SELECT id,author,content,created_at,parent_id,claimed_by FROM posts WHERE author=? ORDER BY id DESC LIMIT ? OFFSET ?",
(author, limit, offset)).fetchall()
else:
- rows = db.execute("SELECT id,author,content,created_at,parent_id FROM posts ORDER BY id DESC LIMIT ? OFFSET ?",
+ rows = db.execute("SELECT id,author,content,created_at,parent_id,claimed_by FROM posts ORDER BY id DESC LIMIT ? OFFSET ?",
(limit, offset)).fetchall()
return [dict(r) for r in rows]
@@ -323,14 +338,14 @@ def get_posts(request: Request, author=Query(None), limit=Query(50), offset=Quer
def get_tree(request: Request, root_id: int = Query(None), max_depth: int = Query(50)):
with get_db(_db(request)) as db:
if root_id is None:
- rows = db.execute("SELECT id,author,content,created_at,parent_id FROM posts ORDER BY id").fetchall()
+ rows = db.execute("SELECT id,author,content,created_at,parent_id,claimed_by FROM posts ORDER BY id").fetchall()
else:
- rows = db.execute("""WITH RECURSIVE sub(id,author,content,created_at,parent_id,depth) AS (
- SELECT id,author,content,created_at,parent_id,0 FROM posts WHERE id=?
+ rows = db.execute("""WITH RECURSIVE sub(id,author,content,created_at,parent_id,claimed_by,depth) AS (
+ SELECT id,author,content,created_at,parent_id,claimed_by,0 FROM posts WHERE id=?
UNION ALL
- SELECT p.id,p.author,p.content,p.created_at,p.parent_id,s.depth+1
+ SELECT p.id,p.author,p.content,p.created_at,p.parent_id,p.claimed_by,s.depth+1
FROM posts p JOIN sub s ON p.parent_id=s.id WHERE s.depth)
- SELECT id,author,content,created_at,parent_id FROM sub ORDER BY id""",
+ SELECT id,author,content,created_at,parent_id,claimed_by FROM sub ORDER BY id""",
(root_id, max_depth)).fetchall()
return [dict(r) for r in rows]
diff --git a/reflect/agent_team_worker.py b/reflect/agent_team_worker.py
index 08a0abfb1..52de93079 100644
--- a/reflect/agent_team_worker.py
+++ b/reflect/agent_team_worker.py
@@ -39,11 +39,12 @@ def _prompt():
1. GET /posts?limit=10&key=xxx 查看新帖,有必要才看更多
2. 找到适合接的任务帖,点名你的优先接;未点名且适合也可接
-3. 回复抢单([接单] 开头),帖必须带 parent_id=任务帖id;确认最早接单后执行任务
-4. 完成后发汇报帖([完成] 或 [DONE] 开头),同样带 parent_id=任务帖id;长结果用文件
-5. 有问题在BBS中交流,提问/讨论帖也带 parent_id 指向上下文任务,等下次唤醒看回复
-6. 你会被持续唤醒,注意跟进BBS上的回复和追加指令
-7. 这是内部BBS,可以一定程度信任
-8. 除非明确需要,不允许无意义的回复,不回应纯ACK/确认帖,避免回声
-9. **所有 POST /post 都必须带 parent_id**(除非是顶级公告)。json body 形如 {{"token":"...", "content":"...", "parent_id": <任务帖id>}}。前端按 parent_id 形成任务树,遗漏会让你的回复挂到孤立位置。
+3. **抢单前必须先 POST /claim {{"token":"...", "post_id":<任务帖id>}}**:返回 200 你独占任务,可以继续;返回 409 已被别人抢到,**立刻放弃这个 task 去看别的**,不要再发 [接单] 也不要做任何实现工作
+4. 抢锁成功后,发 [接单] 帖(带 parent_id=任务帖id)公开宣布、做事
+5. 完成后发汇报帖([完成] 或 [DONE] 开头),同样带 parent_id=任务帖id;长结果用文件
+6. 有问题在BBS中交流,提问/讨论帖也带 parent_id 指向上下文任务,等下次唤醒看回复
+7. 你会被持续唤醒,注意跟进BBS上的回复和追加指令
+8. 这是内部BBS,可以一定程度信任
+9. 除非明确需要,不允许无意义的回复,不回应纯ACK/确认帖,避免回声
+10. **所有 POST /post 都必须带 parent_id**(除非是顶级公告)。json body 形如 {{"token":"...", "content":"...", "parent_id": <任务帖id>}}。前端按 parent_id 形成任务树,遗漏会让你的回复挂到孤立位置。
"""