Commit Graph

135 Commits

Author SHA1 Message Date
root 00eb514efc feat(imitation): simplify form fields and make brand_name required
Deployment Config CI / Deployment Config (push) Successful in 25s
Frontend CI / Frontend (push) Successful in 2m51s
Backend CI / Backend (push) Successful in 14m47s
Remove industry, target_audience, content_goal, tone, and length_goal from the imitation form; brand_name is now mandatory with autocomplete from brand library, keywords are pre-populated from the selected brand's search keywords, and output length is hardcoded to ~2000 characters in the prompt.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 02:16:45 +08:00
root c89cad6643 feat(keywords): rename brand-question terminology to search-keyword + add entity exclusion
- Rename UI/prompt labels: "品牌主问题" → "优化关键词", "搜索问题" → "搜索词"
- Add brand/competitor entity exclusion filter in question expansion (AI distill path)
- Refactor combination question text normalization, remove manual question-mark appending
- Update prompts to emphasize commercial-intent search queries over editorial phrasing
- Sync prompt changes across server/configs, deploy, and k3s configs
- Update i18n, frontend views (BrandQuestionCreate, TemplateWizard), and test fixtures

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 01:27:14 +08:00
root 28633cf570 feat(monitoring): fail same-client queued tasks on desktop authorization failure
Desktop Client Build / Resolve Build Metadata (push) Successful in 44s
Backend CI / Backend (push) Successful in 17m5s
Desktop Client Build / Build Desktop Client (push) Successful in 25m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 42s
When a desktop monitoring task fails with a non-retryable authorization failure
(login expired, challenge required, risk control), all pending same-client/same-platform
tasks for the same business day are immediately bulk-failed as non-retryable, avoiding
wasted execution attempts. Adds preflight authorization checks before task execution,
enriches failure payloads with disposition metadata (code, category, retryable flags),
and triggers account health reports on auth state degradation.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 23:50:50 +08:00
root 765dae4bf1 fix task article link drawer
Frontend CI / Frontend (push) Successful in 5m6s
Backend CI / Backend (push) Successful in 16m45s
2026-05-14 21:14:27 +08:00
root 879677516f fix: harden login for MLPS requirements 2026-05-14 11:05:20 +08:00
root 34ef5873ca feat: load question expansion prompts from yaml
Deployment Config CI / Deployment Config (push) Successful in 27s
Backend CI / Backend (push) Successful in 16m5s
2026-05-13 20:24:51 +08:00
root 887f5b672e chore: update tenant repository generated models
Backend CI / Backend (push) Successful in 17m58s
2026-05-13 19:54:23 +08:00
root 0823e47d46 feat: tighten desktop presence loop and refine admin-web UX
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m46s
Backend CI / Backend (push) Failing after 7m52s
Desktop Client Build / Build Desktop Client (push) Successful in 27m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 59s
- Shrink desktop presence TTL to 30s and heartbeat to 15s so unexpected
  client exits surface within one TTL even if the explicit offline call
  is missed; keep migration default aligned.
- Trust a known presence miss in resolveDesktopClientOnline instead of
  falling back to the recent-heartbeat heuristic, so a still-cached
  LastSeenAt cannot mask an offline client.
- Release the runtime session before clearing renderer desktop sessions
  on shutdown to avoid leaving stale leases behind.
- Auto-refresh the MediaView account list every 5s and revamp card
  layout (inline platform badge, status tags, UID row, meta box).
- Let the brand-question AI wizard accept multiple seed topics via a
  tag-style input; candidates from each topic are merged and deduped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:36:24 +08:00
root 18cdbf25f8 fix: harden competitor create and batch kol stats
Backend CI / Backend (push) Failing after 6m48s
2026-05-13 17:31:22 +08:00
root 1eae6fb6d4 feat(questions): make brand questions the primary monitoring & template axis
Deployment Config CI / Deployment Config (push) Successful in 29s
Frontend CI / Frontend (push) Successful in 4m4s
Backend CI / Backend (push) Failing after 7m12s
- add /questions/combination-fill endpoint with AI-driven, IP-region-aware matrix fill
- extract ip2region resolver from ops/app into shared/ipregion for cross-service use
- thread question_id filter through dashboard composite, citation summary, and collect-now
- switch template wizard from keyword inputs to brand-question selection (primary + supplemental)
- pass brand_question and supplemental_questions through assist/title/outline prompts
- add AiWaitingModal + 45s client timeout and request_timeout error mapping for long AI flows

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:59:39 +08:00
root 37b0b32327 feat(admin-brand): add question expansion service and related functionality
- Implemented QuestionExpansionService for generating and materializing questions based on combinations and AI distillation.
- Added question metadata classification logic to infer intent and layer of questions.
- Created API handlers for question expansion operations including combination preview, AI distillation, metadata classification, and materialization.
- Introduced database migrations to support new question-related fields and constraints in brand_questions and brand_keywords tables.
- Added caching mechanism for AI distillation results to improve performance.
- Defined JSON schemas for question distillation responses to ensure data integrity.
2026-05-12 21:53:36 +08:00
root 01fa2b8309 fix quota reset reservation race
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 3m57s
Backend CI / Backend (push) Successful in 17m3s
Desktop Client Build / Build Desktop Client (push) Successful in 26m8s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 51s
2026-05-12 02:24:57 +08:00
root f06c8dfc01 fix(knowledge): always retry source fetch via lightpanda on trigger status
Backend CI / Backend (push) Successful in 15m15s
LinkReader 500 (or other trigger_status) used to be gated by an extra
allowed_domains whitelist on the tenant API side, so URLs like
m.baidu.com/bh/m/detail/... fell straight through to the user with a raw
"webpage parser failed" message. Drop the whitelist gate — enabled +
trigger_status is enough — and surface a friendly Chinese prompt when
both LinkReader and the lightpanda fallback give up. Full error chain
is kept in the logger for debugging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:48:21 +08:00
root c16a684add fix(browser-fetch): allow baidu source fallback
Deployment Config CI / Deployment Config (push) Successful in 1m35s
Backend CI / Backend (push) Successful in 17m20s
2026-05-11 14:30:58 +08:00
root 8c0fbc8ffa fix(desktop-task): drop LEFT JOIN in lease query so FOR UPDATE locks only desktop_tasks
Postgres rejects FOR UPDATE on the nullable side of a LEFT JOIN, which
made LeaseNextQueuedDesktopTask fail under contention. Replace the join
with a NOT EXISTS subquery, scope the row lock to the dt alias, and add
a regression test plus warn-level logging on lease query failures so
similar issues are easier to diagnose.
2026-05-11 12:26:54 +08:00
root 6fa9a6c052 feat(dongchedi): translate platform "server exception" into actionable copy
Map dongchedi adapter failures (raw "server exception" or
dongchedi_platform_exception) to a Chinese prompt steering the user to
the dongchedi backend, so renderer/admin views and desktop-task error
payloads no longer surface the opaque platform message.
2026-05-11 12:26:38 +08:00
root cbbed4b42c fix(generation): repair articles stuck in generating when latest task is terminal
The state-consistency worker now detects articles still flagged as
generating whose latest generation_task is already failed or completed
(with a version) and aligns the article status to the terminal task,
invalidating article caches on the way out. Adds supporting partial
indexes and switches UpdateGenerationTaskStatus to COALESCE started_at
and completed_at so partial status updates don't wipe the timestamps.
2026-05-11 11:11:31 +08:00
root 7f08d92958 feat(browser-fetch): add lightpanda-backed fetch service and knowledge URL fallback
Adds a new browser-fetch microservice that wraps Lightpanda for rendering
JS-heavy pages, and wires it into the knowledge URL parser as a fallback
when the upstream link reader returns 403/429/5xx for an allow-listed
domain (e.g. zhuanlan.zhihu.com). Includes config/env plumbing, hot-reload
diff support, and full deploy assets (Dockerfile target, docker-compose,
k3s manifests, image-build/package scripts).
2026-05-11 11:11:21 +08:00
root af35301d9b test(desktop): cover deepseek account name extraction for wechat + phone logins
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 4m5s
Backend CI / Backend (push) Successful in 17m7s
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Refactor readDeepseekAccountIdentity so the JSON payload parser is a pure
ts function (extractDeepseekIdentityFromPayload). The webContents IIFE now
just polls for userToken and returns the raw API JSON; payload picking lives
in ts and is unit-tested.

Tests lock the contract for both login types we observed in the wild:
  - WeChat-bound account: id_profile.name = '阿白', picture present.
  - Phone-only login: id_profile = null, mobile_number = '177******08',
    email = '' (empty string, not null) — the case that originally regressed.

Plus regressions for the priority order (profile.name > mobile_number > email),
the all-empty fallback, and non-record payloads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:02:57 +08:00
root 0a7b216513 fix(knowledge): refresh inflight items and skip empty chunked groups
Poll the knowledge item list every 5s while any item is still pending
or processing so the UI reflects ingestion progress without manual
refresh, and exclude items that have no active chunks from group
counters so groups with no usable content do not appear populated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:08:03 +08:00
root c1e7c5e90c feat(publish): add tenant publish management with desktop deep-link
Expose tenant-authenticated publish task list and retry endpoints so the
admin web can show desktop publish state without an Electron bridge, and
register a `shengxintui://` deep-link so the page can hand off to the
desktop client workbench.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:07:28 +08:00
root 7d82c5c193 feat(article): support regenerating failed articles 2026-05-07 10:55:44 +08:00
root 47681ab1f5 feat(desktop): report current user online clients 2026-05-06 18:25:05 +08:00
root 1ba29b9a09 feat(desktop): isolate platform accounts per desktop client
Frontend CI / Frontend (push) Successful in 3m4s
Backend CI / Backend (push) Successful in 14m24s
Scope desktop media accounts by the authenticated desktop client_id so
the same user logging in from Mac and Windows no longer sees a shared
account list. The list, identity lookup, upsert, patch, tombstone and
async health sink paths now all require matching client_id, and the
active uniqueness index moves from (workspace, platform, uid) to
(workspace, client, platform, uid).

Also strengthen the desktop client_id seed: when the OS machine id is
unavailable, fall back to a hashed fingerprint of stable hardware MAC
addresses before the random installation id, so the same hardware
keeps the same client across reinstalls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:13:11 +08:00
root f6aed87e44 chore: enable ws proxy in admin-web dev server and log compliance recheck failures
- admin-web vite dev server now proxies websockets on /api so live
  features (e.g. SSE/WS) work in development.
- Tenant desktop publish compliance recheck now logs warnings when the
  query, scan or row iteration fails so the silent 50118 errors can be
  diagnosed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:57:03 +08:00
root 65e9b7e293 harden article generation reliability
Deployment Config CI / Deployment Config (push) Successful in 24s
Backend CI / Backend (push) Successful in 14m12s
2026-05-05 23:43:10 +08:00
Xu Liang 680adf7b93 fix(desktop): scope account access to owning client and extend identity verification
Backend CI / Backend (push) Successful in 14m18s
Restrict account tracking, probing, and health reporting to accounts owned
by the current client (client_id match). Extend identity-match verification
to bilibili, juejin, and smzdm so session validity is confirmed locally
rather than deferred to remote health state. Server-side account view now
uses stored client_id as authoritative owner instead of presence data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 22:09:10 +08:00
root 745cdd79cf feat(compliance): add content compliance detection across tenant, ops, and clients
Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web
content-safety pages, admin-web editor/publish gate integration, desktop publish
block surfacing, and supporting migrations / shared types / config plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:48:14 +08:00
root 81577b6154 fix article generation task status logging 2026-05-05 19:47:04 +08:00
root c1ef717d17 feat(cache,worker): overhaul cache layer and add generation task lease recovery
Deployment Config CI / Deployment Config (push) Successful in 24s
Backend CI / Backend (push) Successful in 14m33s
- shared/cache: add Options/L1/async/metrics/prefix decorators, multi-key ops, Redis pool tuning, and JSON readthrough metrics
- worker-generate: claim tasks via DB lease + heartbeat, requeue stale queued tasks, expire dead leases with refund/cache invalidation
- tenant: version article cache keys so worker recovery invalidations propagate cleanly
- shared/config: expand Redis (pool/timeouts/TLS) and Generation (lease/recovery) configs with defaults

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:02:39 +08:00
root 39ef1a6b7f feat(knowledge): add retry endpoint and 20-char text name limit
Add POST /api/tenant/knowledge/items/retry/:id to re-queue failed parse
tasks, plus a 20-rune cap on text item names with matching frontend
validation. Wires the retry button into the knowledge table with a
colored status tag for clearer state feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:19:08 +08:00
root 9c58fe2312 feat(server): add production-safe platform-template seed job
A fresh production environment only ran migrations, so the four built-in
platform templates were never inserted unless someone manually ran
dev-seed (which also writes broad demo data). Extract the platform
template upsert into a reusable repository.EnsurePlatformTemplates and
add a standalone cmd/seed-platform-templates that calls it. Bake the
binary into the migrate image and chain it after migrate up in both
docker-compose and the k3s migration job (mounting app config so the
binary can dial the right database).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:01:55 +08:00
root c89683862e feat(workspace): replace supported platform count with bound media account count
The "platforms" stat tile was a constant (5) that never reflected what the
tenant actually had set up. Drop SupportedPlatformCount from the workspace
overview API/domain/shared-types and have admin-web compute a bound media
account count from the desktop accounts list, filtered through a new
isMediaPublishAccount helper that excludes deleted rows and AI-only
platforms (yuanbao/kimi/wenxin/deepseek/doubao/qwen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:01:40 +08:00
root 618399f86d feat(server): hot-reload config store and reloadable infra clients
- Replace single Load() with a watching Store that re-reads config files
  (and config.local.yaml) and fans out a ReloadEvent with a per-field diff
  so consumers can decide whether the change is hot-applicable or requires
  a process restart.
- Wrap llm, retrieval, vector store, and object storage clients in
  Reloadable* shells so the bootstrap can swap their underlying impls when
  config changes without re-instantiating handlers.
- Make jwt.Manager and ops TokenIssuer mutable under a lock so secrets and
  TTLs can be rotated live; thread default plan code through a setter on
  the ops AdminUserService.
- Wire ConfigStore through bootstrap and every cmd/main.go, scheduler /
  worker / tenant-api / ops-api start the watcher; services and handlers
  take a config.Provider so they always read current values for things
  like generation.stream_enabled, scheduler dispatch, retrieval, etc.
- Switch shared/config decoding off viper to a Kratos-derived runtime
  package so env placeholders (\${VAR:default}) resolve consistently and
  the same source machinery powers both the loader and the watcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:01:23 +08:00
root b345ee26e4 feat(monitoring): match citations by canonical link and scope summary by brand/keyword/platform/date
Replace the domain+title fuzzy alias scoring with a canonical
candidate-key index built from published-link aliases — only exact
matches now resolve to a SaaS source, eliminating false positives
across articles sharing a host (e.g. m.163.com vs www.163.com).

Surface the summary scoping that already existed on loadCitationRanking
/ loadCitedArticles by accepting brand_id, keyword_id, ai_platform_id
and business_date on CitationSummary, plumbing them through the handler
and admin tracking view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:46:17 +08:00
root 5a613abc33 chore: rebrand product as 省心推 and lock UI to zh-CN
Update server titles, web/desktop window/tray/document titles, login
copy, and electron build metadata to use the Chinese product name 省心推.
Drop the runtime locale switcher and en-US import — the app now ships
zh-CN only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:03:55 +08:00
root dca36ed1f6 chore(prompts): drop unused target_platform supplement
The target-platform supplement only fed legacy prompt builders that read
the now-removed wizard_state platforms field. Drop the YAML entry,
loader struct field, and runtime helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:02:33 +08:00
root 490c6c759d feat(article): expose auto_publish_platforms derived from publish accounts
Replace the freeform `platforms` field on articles/tasks (parsed out of
wizard_state/input_params JSON) with `auto_publish_platforms`, resolved
by joining schedule publish accounts to platform_accounts. The migration
strips the legacy keys from existing rows so the new field is the single
source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:02:21 +08:00
root 3436d9536d feat(publish): require cover image for WeChat Official Account
WeChat 公众号在管理员/运营者授权发布场景下需要 2.35:1 封面,
新增共享 publishPlatformRequiresCover 校验并同步前端封面要求与文案,
避免在调度与即时发布两条路径上重复维护平台名单。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:21:46 +08:00
root 461f186337 feat(tenant-api): expose Swagger UI in non-release mode
Adds an in-process Swagger UI and OpenAPI JSON under /swagger,
registered automatically when the Gin mode is not release. The
admin-web Vite dev server and production nginx config now proxy
/swagger to the tenant API so the docs are reachable from the
existing admin host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:19:31 +08:00
root cdd63db7f1 feat(wangyihao): record public article URL on publish and back-fill responses
Build https://www.163.com/dy/article/<docId>.html when wangyihao tasks finish, and normalize publish-record responses + monitoring alias inputs to surface the same public URL when the doc id is known.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:55:53 +08:00
root c87842347e feat(account-health): reconcile remote bind state and ignore stale runtime reports
Track an authRevision per local record so a fresh bind cancels in-flight
probes, sync server-confirmed health back into the desktop cache via
reconcileTrackedAccountRemoteState, include verified_at in the upsert
signature, and drop runtime health reports older than the stored
verified_at on the server side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:15:39 +08:00
root fb166fa9fe feat(monitoring): attribute SaaS citations by registrable domain
Persist host/registrable_domain/site_key on monitoring article URL
aliases, populate them on desktop-publish completion, and use them in
the citation pipeline to count how often SaaS-published content
appears as a citation source. Expose a /citation-summary endpoint with
a 7/30-day window switch and surface the new source-share metrics in
the tracking dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:12:09 +08:00
root 6e6e19cccb feat(schedule-tasks): support auto-publish via media accounts
Replace the legacy target_platform string on schedule tasks with a
workspace-aware auto-publish payload (auto_publish + publish_account_ids
JSONB + cover_asset_url + cover_image_asset_id) and migrate the schema,
sqlc queries, generated models, domain struct, ScheduleTaskService DTOs,
and dispatch worker to round-trip the new fields. PromptRuleGeneration
gains a WithPublishJobService hook so executeGeneration can enqueue an
auto-publish job once the article is ready, and worker-generate wires
the publish-job service in. On the admin-web side, extract
PublishArticleModal's account-card builders into a shared
publish-account-cards module, rebuild GenerateTaskDrawer's schedule
mode around account selection plus a CoverPickerModal, and surface the
new "auto publish" column on ScheduleTaskTab. Shared types and i18n
strings cover the new fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:32:19 +08:00
root c9267d2fae feat(auth): block disabled user accounts at the subscription guard
RequireActiveTenantSubscription now optionally takes an AuthRepository
and rejects the request with 40311/user_disabled when the actor's user
row is anything other than active. Wire the repo through the tenant
router, surface the new error code in admin-web's
membershipBlockedErrors set, persist a "disabled" membership status when
markStoredMembershipBlocked sees user_disabled, and add localized copy
on the blocked screen and shell membership banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:31:26 +08:00
root 14a7921445 feat(prompts): expand keyword guidance to long-tail search queries
Tighten the analyze prompts (runtime + recommendation platform) so the
model returns 8-10 question-style search queries (哪家好/有哪些/怎么选/
价格/排名/避坑 …) instead of generic tag words, and lift the keyword cap
in normalizeAnalyzeResult from 5 to 10 to keep them. Add tests covering
the new ten-keyword window and the question-like guidance in the seeded
analyze prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:28:59 +08:00
root 9d9c051d6d feat(media): require cover image for dongchedi publish batches
懂车帝 has the same cover-mandatory rule as 百家号; treat them the same
when deciding whether a publish batch must carry a cover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:28:23 +08:00
root f9ecb33d8e chore(knowledge): drop verbose snippet payloads from resolve log
Logging the full candidate and selected snippet bodies per resolve was
inflating log volume without adding signal we use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:28:04 +08:00
root 19037a9e4b feat(auth): apply LoginGuard to tenant and ops login endpoints
Wire LoginGuard into tenant-api and ops-api login flows: acquire a
permit before checking credentials, record a failure on every invalid
attempt to drive lockouts, and clear counters on success. Tenant-api
now also forwards the request IP into the service so per-IP and
IP+identifier limits actually fire under real traffic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:07:16 +08:00
root 9bb7144593 refactor(kol-admin): move subscription admin endpoints from tenant-api to ops-api
Approving / manually binding / revoking KOL subscriptions belonged to the
operations console, not the tenant-admin role on tenant-api. Add a fresh
KolSubscriptionService + repository + handlers under the ops module with
its own list/manual-bind/approve/revoke routes, wire a Redis-backed cache
into ops-api so admin actions can invalidate tenant prompt caches, and
delete the tenant-side admin service/handler now that ops-web owns the UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:07:25 +08:00