Compare commits

..

6 Commits

Author SHA1 Message Date
陈世睿 0964d81898 refactor(device): device 表→device_liveness + liveness 接口只返 bool(review 意见)
- 表/类改名 device→device_liveness / Device→DeviceLiveness:该表记的是无障碍存活监控状态
  (心跳/liveness_state/kill_alert_pending/推送目标),非设备信息(品牌/型号),原名误导。同步改
  建表迁移表名/索引/约束(未部署→直接改建表迁移,非加 rename);API 路径 /api/v1/device 不变。
- GET /device/liveness 的 LivenessOut 只返 kill_alert_pending 一个布尔(原返回 liveness_state/
  ever_protected/last_heartbeat_at 等设备详情);前端本就只读 kill_alert_pending,不受影响。
2026-06-23 10:20:02 +08:00
陈世睿 f4f1ad6f22 Merge branch 'main' of https://gitea.shaguabijia.com/WonderableAI/shaguabijia-app-server into feat/a11y-liveness-pull 2026-06-23 09:48:54 +08:00
陈世睿 26360dd621 feat(ad): 信息流每条发奖(一条=一个公式值,因子2按条) + 记录金币 trace_id 归属
- 信息流发奖:一条广告 = 一个 calculate_ad_reward_coin(因子2 按账号累计条数),不再逐份累加
- 新增 GET /api/v1/ad/feed-reward/units 返回累计已发条数,供前端算因子2 做实时金币进度条
- 比价记录金币:feed_ad_reward 加 trace_id 列(迁移)按 trace_id 归属到比价记录;记录接口聚合金币
- chore: HEARTBEAT_TIMEOUT_MINUTES 测试值 2 回 10
2026-06-23 00:27:47 +08:00
陈世睿 227e00184f Merge branch 'main' of https://gitea.shaguabijia.com/WonderableAI/shaguabijia-app-server into feat/a11y-liveness-pull 2026-06-22 10:03:09 +08:00
陈世睿 9f8ffadaa3 feat(compare): 逐平台 status/store_closed 落库 + platform_results 收 dict
- ComparisonResultIn 加 status / store_closed, 记录页按平台显示异常文案
- platform_results 声明为 dict(pricebot 按 platform_id 键的 dict), 修成功记录 422 被拒

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:49:55 +08:00
陈世睿 2162056008 feat(device): 无障碍保护存活心跳检测 + 掉线后置检测(pull)
- device 表 + 心跳/注册/liveness 查询/ack 端点 + heartbeat_monitor_worker
  (扫描超时设备置 kill_alert_pending, 客户端进 App 拉取弹自启动引导)
- 本期只做 pull 后置检测, 不接推送(不含 jpush)
- 迁移: device_table → device_kill_alert_pending(接 main head ceb286289426, 线性单 head)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:49:54 +08:00
6 changed files with 26 additions and 110 deletions
@@ -1,26 +0,0 @@
"""merge device#65 and comparison_token_cols heads
Revision ID: 4dc2af7ebe74
Revises: comparison_token_cols, ad_feed_reward_trace_id
Create Date: 2026-06-23 17:57:54.083531
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '4dc2af7ebe74'
down_revision: Union[str, Sequence[str], None] = ('comparison_token_cols', 'ad_feed_reward_trace_id')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
pass
def downgrade() -> None:
pass
-37
View File
@@ -1,37 +0,0 @@
"""add input_tokens / output_tokens to comparison_record
比价记录增加本次 LLM 累计 token(server 从 llm_calls[].usage 累加),
供 admin 比价记录页展示 token 数与估算成本。
Revision ID: comparison_token_cols
Revises: feedback_review_fields
Create Date: 2026-06-23 00:40:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "comparison_token_cols"
down_revision: Union[str, Sequence[str], None] = "feedback_review_fields"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"comparison_record",
sa.Column("input_tokens", sa.Integer(), nullable=True),
)
op.add_column(
"comparison_record",
sa.Column("output_tokens", sa.Integer(), nullable=True),
)
def downgrade() -> None:
op.drop_column("comparison_record", "output_tokens")
op.drop_column("comparison_record", "input_tokens")
+25 -34
View File
@@ -474,53 +474,44 @@ def group_order_daily(
def group_wx_users(db: Session, *, group_id: int, limit: int = 200) -> list[dict]:
"""该群来过的微信用户:在本群有带 openid 点击的所有用户 + 本群 visit/copy 次数 + 画像。
"""群内微信用户(首次来源 first_group_id=group_id)+ 每人在该群的 visit/copy 次数(领券画像)
用「本群点击」判定归属,而非 cps_wx_user.first_group_id —— 同一用户会在多个群活动,
first_group_id 只记首次授权群,会漏掉"先在别处授权、cookie 已存、又来本群"的用户。
copy=本群点「复制口令」次数(领券意向),visit=进落地页次数。按本群首次点击倒序。
昵称/头像来自授权画像;美团/京东群只 302 跳转、无 userinfo 触发点,故多为空。
按首次进入倒序。copy 次数 = 该用户在该群点了几次「复制口令」(领券意向),visit = 进落地页次数。
"""
users = (
db.execute(
select(CpsWxUser)
.where(CpsWxUser.first_group_id == group_id)
.order_by(desc(CpsWxUser.first_seen))
.limit(limit)
)
.scalars()
.all()
)
if not users:
return []
openids = [u.openid for u in users]
stat: dict[str, dict] = {}
rows = db.execute(
select(CpsClick.openid, CpsClick.event_type, func.count())
.where(CpsClick.group_id == group_id)
.where(CpsClick.openid.is_not(None))
.where(CpsClick.openid.in_(openids))
.group_by(CpsClick.openid, CpsClick.event_type)
).all()
if not rows:
return []
stat: dict[str, dict] = {}
for openid, event_type, cnt in rows:
s = stat.setdefault(openid, {"visit": 0, "copy": 0})
if event_type == "copy":
s["copy"] = cnt
else:
s["visit"] = cnt
openids = list(stat.keys())
# 本群每个 openid 的首次点击时间(排序 + 展示"首次进入")
first_seen = dict(
db.execute(
select(CpsClick.openid, func.min(CpsClick.clicked_at))
.where(CpsClick.group_id == group_id)
.where(CpsClick.openid.is_not(None))
.group_by(CpsClick.openid)
).all()
)
# 关联画像(未授权 userinfo 的昵称/头像为空)
users = {
u.openid: u
for u in db.execute(select(CpsWxUser).where(CpsWxUser.openid.in_(openids))).scalars().all()
}
result = [
return [
{
"openid": openid,
"nickname": users[openid].nickname if openid in users else None,
"headimgurl": users[openid].headimgurl if openid in users else None,
"first_seen": first_seen.get(openid),
"visit_count": stat[openid]["visit"],
"copy_count": stat[openid]["copy"],
"openid": u.openid,
"nickname": u.nickname,
"headimgurl": u.headimgurl,
"first_seen": u.first_seen,
"visit_count": stat.get(u.openid, {}).get("visit", 0),
"copy_count": stat.get(u.openid, {}).get("copy", 0),
}
for openid in openids
for u in users
]
result.sort(key=lambda x: x["first_seen"], reverse=True)
return result[:limit]
-2
View File
@@ -33,8 +33,6 @@ class AdminComparisonListItem(BaseModel):
step_count: int | None = None
llm_call_count: int | None = None
retry_count: int | None = None
input_tokens: int | None = None # Σ usage.prompt_tokens(server 派生)
output_tokens: int | None = None # Σ usage.completion_tokens(server 派生)
device_model: str | None = None
rom_vendor: str | None = None
rom_name: str | None = None
+1 -8
View File
@@ -77,15 +77,8 @@ def _backfill_llm_calls(record_id: int, trace_id: str) -> None:
rec.llm_calls = calls
rec.llm_call_count = len(calls)
rec.retry_count = sum(1 for c in calls if c.get("error"))
# token 累加(usage 已被 pricebot llm_client 归一为 prompt/completion_tokens;
# error 的调用 usage 可能为 None,or {} 兜底)
rec.input_tokens = sum((c.get("usage") or {}).get("prompt_tokens") or 0 for c in calls)
rec.output_tokens = sum((c.get("usage") or {}).get("completion_tokens") or 0 for c in calls)
db.commit()
logger.info(
"backfill llm_calls trace=%s n=%d in_tok=%d out_tok=%d",
trace_id, len(calls), rec.input_tokens, rec.output_tokens,
)
logger.info("backfill llm_calls trace=%s n=%d", trace_id, len(calls))
except Exception as e: # noqa: BLE001 best-effort
logger.warning("backfill llm_calls failed trace=%s: %s", trace_id, e)
finally:
-3
View File
@@ -124,9 +124,6 @@ class ComparisonRecord(Base):
step_count: Mapped[int | None] = mapped_column(Integer, nullable=True) # 客户端 agent loop 总步数
llm_call_count: Mapped[int | None] = mapped_column(Integer, nullable=True) # 本次 LLM 调用次数(server 从 llm_calls 算)
retry_count: Mapped[int | None] = mapped_column(Integer, nullable=True) # LLM 失败重试次数(server 从 llm_calls error 算)
# 本次 LLM 累计 token(server 从 llm_calls[].usage 累加;旧记录/未采集为 None)
input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) # Σ usage.prompt_tokens
output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) # Σ usage.completion_tokens
# 每次 LLM 调用明细 [{scene,model,input_messages,output,usage,latency_ms,error}];
# server 收上报后按 trace_id 同机拉 pricebot 落库(见 compare_record 端点)。旧记录/未采集为 None。
llm_calls: Mapped[list | None] = mapped_column(_JSON, nullable=True)