diff --git a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx index 4c56e940..2cd88cd4 100644 --- a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx +++ b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx @@ -414,6 +414,8 @@ function AIWritingBody({ const routeBase = useAIWritingRouteBase(); const agentRoute = useAIWritingAgentRouteState(routeBase); const isAdmin = getStoredAuth()?.system_role === 'admin'; + const externalNotebookMode = + studioToken != null || initialNotebookId != null || initialDocumentIds != null; // 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容 // (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。 @@ -432,12 +434,12 @@ function AIWritingBody({ } }, [agentRoute.active]); - const agentsActive = sidebarTab === 'agents'; + const agentsActive = !externalNotebookMode && sidebarTab === 'agents'; // 研究模板侧栏在「写作配置(idle)」与「历史回看」两态都展示(点历史记录后保留侧栏、 // 历史内容显示到右侧区域);写作进行中收起;嵌入态(studioToken)始终不展示。 const showSidebar = - !studioToken && (state.status === 'idle' || isHistoryMode || agentsActive); + !externalNotebookMode && (state.status === 'idle' || isHistoryMode || agentsActive); const compactAppearanceActions = compactChrome ? ( ) : null; diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 1cb9bcd3..b8eba50b 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -273,7 +273,7 @@ The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four s Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. -**Skill-driven retrieval (检索类型).** `material_source` has three values: `general` (通用检索, default), `knowledge_base` (知识库检索), and `notebook` (我的空间); legacy `skill` is normalized to `general` by the researcher node. **`knowledge_base` 直连内网 ES 接口(= 旧版「内网直连检索」)** — it bypasses skill orchestration entirely and calls `IntranetSearchProvider` over `ai_writing.intranet_search_url` (concurrent over the generated keywords; `intranet_knowledge_base`/`intranet_search_timeout` apply). **An empty/unconfigured `intranet_search_url` does NOT disable it** — the empty string is passed straight to `IntranetSearchProvider`, which falls back to its built-in default endpoint (`intranet_stub._DEFAULT_ENDPOINT`), exactly like the legacy `get_search_provider(search_provider=intranet)` path. So 内网直连检索 works out of the box without configuring an address (configure `intranet_search_url` only to point at a different endpoint). This is the "弱模型识别不了技能" escape hatch — the old no-skill retrieval path restored as an explicit option (weak models like deepseek-chat often fail to invoke configured skills). Tests: `tests/test_ai_writing_knowledge_base.py`. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). **Skill-failure fault tolerance**: a configured skill that times out / raises / has all its keyword searches fail (vs. legitimately returning 0 results) no longer just silently moves on — the researcher node emits a `skill_search_step` of type `skill_error` (carrying the error `reason`), and if the configured skills collected **no** new materials at all it runs a `fallback` pass over other available skills (`_fallback_candidate_skills`: the built-in `knowledge-base-search` native path first, then other enabled catalog skills not yet tried) instead of erroring out. The frontend `SkillStepsCard` renders `skill_error` (rose) and `fallback` (amber) so the timeline shows "技能报错 → 改用其他技能搜集". Tests: `tests/test_ai_writing_material_source.py`. +**Skill-driven retrieval (检索类型).** `material_source` has three values: `general` (通用检索, default), `knowledge_base` (知识库检索), and `notebook` (我的空间); legacy `skill` is normalized to `general` by the researcher node. **`knowledge_base` 直连内网 ES 接口(= 旧版「内网直连检索」)** — it bypasses skill orchestration entirely and calls `IntranetSearchProvider` over `ai_writing.intranet_search_url` (concurrent over the generated keywords; `intranet_knowledge_base`/`intranet_search_timeout` apply). The client uses `trust_env=False` and defaults `intranet_verify_ssl=false`, so internal HTTPS endpoints with self-signed/internal-CA certificates do not fail on certificate verification; set `ai_writing.intranet_verify_ssl: true` only for environments that require strict certificate checks. **An empty/unconfigured `intranet_search_url` does NOT disable it** — the empty string is passed straight to `IntranetSearchProvider`, which falls back to its built-in default endpoint (`intranet_stub._DEFAULT_ENDPOINT`), exactly like the legacy `get_search_provider(search_provider=intranet)` path. So 内网直连检索 works out of the box without configuring an address (configure `intranet_search_url` only to point at a different endpoint). This is the "弱模型识别不了技能" escape hatch — the old no-skill retrieval path restored as an explicit option (weak models like deepseek-chat often fail to invoke configured skills). Tests: `tests/test_ai_writing_knowledge_base.py`. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). **Skill-failure fault tolerance**: a configured skill that times out / raises / has all its keyword searches fail (vs. legitimately returning 0 results) no longer just silently moves on — the researcher node emits a `skill_search_step` of type `skill_error` (carrying the error `reason`), and if the configured skills collected **no** new materials at all it runs a `fallback` pass over other available skills (`_fallback_candidate_skills`: the built-in `knowledge-base-search` native path first, then other enabled catalog skills not yet tried) instead of erroring out. The frontend `SkillStepsCard` renders `skill_error` (rose) and `fallback` (amber) so the timeline shows "技能报错 → 改用其他技能搜集". Tests: `tests/test_ai_writing_material_source.py`. **Skill-call timeout** is hardcoded **300s (5 min)** — `researcher.py` `_SKILL_SEARCH_TIMEOUT_SECONDS` (per-skill wrapper) and `_skill_agent.py` `_REAL_AGENT_TIMEOUT_SECONDS` (whole real-agent skill run). The intranet ES fallback (`IntranetSearchProvider`) gets a longer **600s (10 min)** default (`intranet_stub.py`; overridable via `ai_writing.intranet_search_timeout`) — that interface is the slow one, so our own fallback timeout is generous to avoid being blamed for "检不出". diff --git a/offline-backend-20260512/backend/README.md b/offline-backend-20260512/backend/README.md index 40e7e03b..229d1cf0 100644 --- a/offline-backend-20260512/backend/README.md +++ b/offline-backend-20260512/backend/README.md @@ -135,6 +135,10 @@ FastAPI application providing REST endpoints for frontend integration: Custom agents are indexed in the `agents` database table. The `id` column is the stable runtime identifier and filesystem directory name (`.deer-flow/agents/{id}/SOUL.md`); `name` is display-only, may be Chinese, and may be duplicated. Rows with `user_id = NULL` are built-in agents, user rows are private unless `published = true`, and users can only update or delete agents they created. +### AI Writing Intranet Retrieval + +AI 写作的「知识库检索」会直连 `ai_writing.intranet_search_url` 内网 ES/知识库接口,并默认跳过代理环境变量和 HTTPS 证书校验,适配自签名或内部 CA 证书的内网服务。需要强校验时可在 `config.yaml` 里设置 `ai_writing.intranet_verify_ssl: true`。 + ### IM Channels The IM bridge supports Feishu, Slack, and Telegram. Slack and Telegram still use the final `runs.wait()` response path, while Feishu now streams through `runs.stream(["messages-tuple", "values"])` and updates a single in-thread card in place. diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py index 77b51ff5..a91d8e0e 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py @@ -27,13 +27,15 @@ from types import SimpleNamespace from langchain_core.runnables import RunnableConfig -from ..search import get_search_provider, SearchResult +from ..prompts.researcher_prompts import ( + EXTRACT_KEYWORDS_SYSTEM, + EXTRACT_KEYWORDS_USER, + RANK_MATERIALS_SYSTEM, + RANK_MATERIALS_USER, +) +from ..search import SearchResult, get_search_provider from ..search.skill_search import SkillPicker from ..state import AIWritingState, Material, MaterialPackage -from ..prompts.researcher_prompts import ( - EXTRACT_KEYWORDS_SYSTEM, EXTRACT_KEYWORDS_USER, - RANK_MATERIALS_SYSTEM, RANK_MATERIALS_USER, -) from ._agent_runner import ( KEYWORDS_READY_MARKER, MATERIALS_RANKED_MARKER, @@ -349,7 +351,10 @@ async def _search_notebook( results: list[Material] = [] mat_idx = 0 try: - async with httpx.AsyncClient(timeout=20) as client: + # trust_env=False:笔记后端是内网/本机地址,但模型走公网,部署常设 + # HTTP_PROXY/HTTPS_PROXY;httpx 默认会把这个内网请求也走代理 → 不可达。 + # 强制忽略代理 env、直连(与内网 ES 检索同款修法)。 + async with httpx.AsyncClient(timeout=20, trust_env=False) as client: for kw in keywords[:_NOTEBOOK_MAX_KEYWORDS]: logger.info("[notebook-search] POST kw=%r body=%s", kw, { "query": kw, @@ -617,6 +622,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict # 内网慢检索超时可在 config.yaml 的 ai_writing.intranet_search_timeout 调大, # 默认 600s,且最低 300s。 timeout=_intranet_timeout_seconds(ai_writing_config), + verify_ssl=ai_writing_config.get("intranet_verify_ssl", False), ) endpoint_label = intranet_url or getattr(provider, "endpoint", "内置默认内网端点") intranet_debug = { @@ -625,6 +631,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict "endpoint": endpoint_label, "knowledge_base": ai_writing_config.get("intranet_knowledge_base", "samples"), "timeout": _intranet_timeout_seconds(ai_writing_config), + "verify_ssl": getattr(provider, "verify_ssl", False), "status": "running", "queries": [], "calls": [], @@ -777,7 +784,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict timeout=_SKILL_SEARCH_TIMEOUT_SECONDS, ) return (results or []), "" - except asyncio.TimeoutError: + except TimeoutError: msg = f"检索超时(>{_SKILL_SEARCH_TIMEOUT_SECONDS}s)" logger.warning("skill[%s] search timed out after %ss", skill_name, _SKILL_SEARCH_TIMEOUT_SECONDS) search_errors.append(f"skill[{skill_name}]: {msg}") @@ -920,13 +927,14 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict intranet_url = (ai_writing_config.get("intranet_search_url") or "").strip() # 诊断开关:开启后即使技能成功也照样跑完内网并记录完整结果(用于验证内网接口是否真能通)。 intranet_debug_force = bool(ai_writing_config.get("intranet_search_debug", False)) - intranet_task: "asyncio.Task | None" = None + intranet_task: asyncio.Task | None = None intranet_holder: dict = {} intranet_debug = { "configured": bool(intranet_url), "endpoint": intranet_url or "(未配置 ai_writing.intranet_search_url)", "knowledge_base": ai_writing_config.get("intranet_knowledge_base", "samples"), "timeout": _intranet_timeout_seconds(ai_writing_config), + "verify_ssl": bool(ai_writing_config.get("intranet_verify_ssl", False)), "debug_force": intranet_debug_force, "status": "pending" if intranet_url else "not_configured", "queries": [], # 每个检索词:{query, count, error?} @@ -949,6 +957,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict # 兜底接口超时可在 config.yaml 的 ai_writing.intranet_search_timeout 调大, # 默认 600s(10 分钟),且最低 300s。 timeout=_intranet_timeout_seconds(ai_writing_config), + verify_ssl=ai_writing_config.get("intranet_verify_ssl", False), ) intranet_holder["provider"] = intranet fb_keywords = keywords[:keyword_count] or [intent] diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/__init__.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/__init__.py index 68a0f36f..f58de660 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/__init__.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/__init__.py @@ -1,8 +1,8 @@ from .base import BaseSearchProvider, SearchResult -from .internet_search import InternetSearchProvider -from .intranet_stub import IntranetSearchProvider, normalize_intranet_timeout from .demo_search import DemoSearchProvider from .duckduckgo_search import DuckDuckGoSearchProvider +from .internet_search import InternetSearchProvider +from .intranet_stub import IntranetSearchProvider, normalize_intranet_timeout, normalize_intranet_verify_ssl from .skill_search import SkillSearchProvider @@ -14,6 +14,7 @@ def get_search_provider(ai_writing_config: dict) -> BaseSearchProvider: intranet_search_url: http://10.106.57.180:7863/knowledge_base/search_docs_es intranet_knowledge_base: samples intranet_search_timeout: 300 # 内网检索超时秒数,最少 300 秒 + intranet_verify_ssl: false # 内网 HTTPS 自签名证书场景默认关闭证书校验 ddg_region: "cn-zh" # DuckDuckGo 区域,默认 cn-zh internet 模式:DuckDuckGo 为主搜索(免 API Key),失败时 fallback 到 demo 演示素材。 @@ -26,6 +27,7 @@ def get_search_provider(ai_writing_config: dict) -> BaseSearchProvider: base_url=cfg.get("intranet_search_url", ""), knowledge_base_name=cfg.get("intranet_knowledge_base", "samples"), timeout=normalize_intranet_timeout(cfg.get("intranet_search_timeout")), + verify_ssl=normalize_intranet_verify_ssl(cfg.get("intranet_verify_ssl")), ) if provider == "tavily": return _FallbackSearchProvider(InternetSearchProvider(), DemoSearchProvider()) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py index ce3e6f2b..4398933d 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging import time -from typing import List from .base import BaseSearchProvider, SearchResult, simplify_query @@ -15,6 +14,8 @@ _DEFAULT_KB = "samples" # 内网检索最少等待 5 分钟;该接口经常慢返回,低于这个值会造成素材收集误判为空。 _MIN_TIMEOUT_SECONDS = 300 _DEFAULT_TIMEOUT_SECONDS = 600 +# 内网 HTTPS 检索服务常使用自签名/内部 CA 证书,默认跳过证书校验避免 SSL 证书链错误。 +_DEFAULT_VERIFY_SSL = False # 单条原始响应体最大保留长度,避免把超大响应整段灌进前端时间线 _RAW_SNIPPET_LIMIT = 2000 @@ -28,6 +29,23 @@ def normalize_intranet_timeout(timeout: object, default: int = _DEFAULT_TIMEOUT_ return max(value, _MIN_TIMEOUT_SECONDS) +def normalize_intranet_verify_ssl(value: object, default: bool = _DEFAULT_VERIFY_SSL) -> bool: + """返回内网检索是否校验 SSL 证书,兼容 config.yaml 里的 bool / 字符串写法。""" + if isinstance(value, bool): + return value + if value is None: + return default + if isinstance(value, (int, float)): + return bool(value) + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + return default + + def _pick(d: dict, keys: tuple) -> str: """从结果项里按候选字段名取第一个非空值(两套接口字段名不同,做并集兼容)。""" for k in keys: @@ -62,12 +80,14 @@ class IntranetSearchProvider(BaseSearchProvider): base_url: str = "", knowledge_base_name: str = _DEFAULT_KB, timeout: int = _DEFAULT_TIMEOUT_SECONDS, + verify_ssl: bool = _DEFAULT_VERIFY_SSL, **_ignored, ): self.endpoint = base_url or _DEFAULT_ENDPOINT self.knowledge_base_name = knowledge_base_name or _DEFAULT_KB # 内网 ES 接口很慢,兜底检索超时默认 10 分钟,且无论配置如何至少 5 分钟。 self.timeout = normalize_intranet_timeout(timeout) + self.verify_ssl = normalize_intranet_verify_ssl(verify_ssl) # 收集本次运行的检索异常,供 researcher 节点取出并提示用户(ES 波动等) self.errors: list[str] = [] # 解析失败时收集的原始响应体,供前端「错误返回结果」折叠展示 @@ -82,7 +102,7 @@ class IntranetSearchProvider(BaseSearchProvider): if snippet: self.raw_responses.append(snippet) - async def search(self, query: str, max_results: int = 5, **kwargs) -> List[SearchResult]: + async def search(self, query: str, max_results: int = 5, **kwargs) -> list[SearchResult]: results = await self._search_once(query, max_results) # 0 结果兜底:复合检索词常检不出(如「特朗普竞选」),换成更核心的检索词重试一次 # (「特朗普竞选」→「特朗普」)。仅在首检为空、且能简化出不同的词时才重试。 @@ -93,7 +113,7 @@ class IntranetSearchProvider(BaseSearchProvider): results = await self._search_once(simpler, max_results) return results - async def _search_once(self, query: str, max_results: int = 5) -> List[SearchResult]: + async def _search_once(self, query: str, max_results: int = 5) -> list[SearchResult]: import httpx payload = { @@ -112,9 +132,22 @@ class IntranetSearchProvider(BaseSearchProvider): "status": "pending", "ok": False, "count": 0, + "verify_ssl": self.verify_ssl, } + # 排查用:每次真实调用都打一行请求明细(端点 / 知识库 / 检索词 / 超时), + # 内网复现时直接看后端日志即可知道"到底请求了哪个地址、用什么参数"。 + logger.info( + "[intranet-search] POST endpoint=%s kb=%s timeout=%ss verify_ssl=%s query=%r", + self.endpoint, self.knowledge_base_name, self.timeout, self.verify_ssl, query, + ) try: - async with httpx.AsyncClient(timeout=self.timeout) as client: + # trust_env=False:**关键**。内网 ES 是内网地址,但本部署的模型走公网, + # 服务器通常设了 HTTP_PROXY/HTTPS_PROXY 出公网;httpx 默认 trust_env=True + # 会把这个内网请求也套用公网代理 → 代理到不了内网 IP → 连接被重置/空回复, + # 表现为「接口异常」。强制忽略代理 env、直连内网(与 multi_agent.py 的回环 + # 调用同款修法)。 + # verify=False:内网 HTTPS 常见自签名/内部 CA 证书;这里仅跳过本内网检索端点的证书校验。 + async with httpx.AsyncClient(timeout=self.timeout, trust_env=False, verify=self.verify_ssl) as client: resp = await client.post(self.endpoint, json=payload) call["http_status"] = getattr(resp, "status_code", None) resp.raise_for_status() @@ -122,23 +155,31 @@ class IntranetSearchProvider(BaseSearchProvider): data = resp.json() except httpx.HTTPStatusError as e: # 非 2xx:接口有响应但状态码异常,把响应体一并暴露给前端 - logger.warning("intranet search failed for query=%r: %s", query, e) + body_for_log = e.response.text if e.response is not None else "" + logger.warning( + "[intranet-search] HTTP error endpoint=%s query=%r status=%s body=%s", + self.endpoint, query, getattr(e.response, "status_code", None), _raw_snippet(body_for_log), + ) message = f"{type(e).__name__}: {e}" self.errors.append(message) - body = e.response.text if e.response is not None else "" call.update({ "status": "http_error", "error": message, "http_status": getattr(e.response, "status_code", None), }) - snippet = _raw_snippet(body) + snippet = _raw_snippet(body_for_log) if snippet: call["raw_response"] = snippet - self._record_raw(body) + self._record_raw(body_for_log) return [] except Exception as e: - # 连接失败 / 超时 / JSON 解析失败等;JSON 解析失败时 raw_text 已是接口响应体 - logger.warning("intranet search failed for query=%r: %s", query, e) + # 连接失败 / 超时 / JSON 解析失败等;JSON 解析失败时 raw_text 已是接口响应体。 + # 打出异常类型,便于区分 ConnectError / ConnectTimeout / ReadTimeout / + # RemoteProtocolError(空回复) / JSONDecodeError(返回非 JSON) 等具体根因。 + logger.warning( + "[intranet-search] request failed endpoint=%s query=%r error=%s: %s", + self.endpoint, query, type(e).__name__, e, + ) message = f"{type(e).__name__}: {e}" self.errors.append(message) call.update({"status": "error", "error": message}) @@ -187,7 +228,11 @@ class IntranetSearchProvider(BaseSearchProvider): self._record_raw(raw_text) return [] - results: List[SearchResult] = [] + logger.info( + "[intranet-search] OK endpoint=%s query=%r http_status=%s raw_items=%s", + self.endpoint, query, call.get("http_status"), len(items) if isinstance(items, list) else 0, + ) + results: list[SearchResult] = [] for i, item in enumerate(items[:max_results]): if not isinstance(item, dict): continue diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/agents/sql.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/agents/sql.py index 4eeddb1f..c1e47b44 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/agents/sql.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/agents/sql.py @@ -6,20 +6,21 @@ from datetime import UTC, datetime from typing import Any from uuid import uuid4 -from sqlalchemy import and_, case, delete, func, or_, select +from sqlalchemy import and_, case, delete, func, literal, not_, or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from deerflow.config.agent_name_desensitize import source_terms_for_needle from deerflow.persistence.agents.base import AgentStore -from deerflow.persistence.agents.model import ( - AgentExtrasRow, - AgentFavoriteRow, - AgentPinRow, - AgentRow, - AgentSkillRow, -) -from deerflow.persistence.tags.model import TagAssignmentRow +from deerflow.persistence.agents.model import ( + AgentExtrasRow, + AgentFavoriteRow, + AgentPinRow, + AgentRow, + AgentSkillRow, +) +from deerflow.persistence.roundtable_chains.model import RoundtableChainRow +from deerflow.persistence.tags.model import TagAssignmentRow def _row_to_dict(row: AgentRow) -> dict[str, Any]: @@ -32,9 +33,33 @@ def _row_to_dict(row: AgentRow) -> dict[str, Any]: return data -class AgentRepository(AgentStore): - def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None: - self._sf = session_factory +class AgentRepository(AgentStore): + def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None: + self._sf = session_factory + + @staticmethod + def _business_chain_agent_exists(agent_id_col): + return ( + select(RoundtableChainRow.id) + .where( + or_( + RoundtableChainRow.seats.like(literal('%"agent_id": "') + agent_id_col + literal('"%')), + RoundtableChainRow.seats.like(literal('%"agent_id":"') + agent_id_col + literal('"%')), + ) + ) + .exists() + ) + + @classmethod + def _valid_favorite_filter(cls): + return or_( + AgentFavoriteRow.origin == "user", + and_( + AgentFavoriteRow.origin == "position", + AgentRow.user_id.is_not(None), + not_(cls._business_chain_agent_exists(AgentFavoriteRow.agent_id)), + ), + ) @staticmethod def _visible_filter(user_id: str): @@ -52,15 +77,23 @@ class AgentRepository(AgentStore): ``published=false``, so this clause cannot lean on ``published``; - favorited — the "我的" listing keeps favorited agents even after the owner unpublishes them, so reads keep honoring the favorite. - """ - fav_subq = select(AgentFavoriteRow.agent_id).where(AgentFavoriteRow.user_id == user_id) - return or_( - AgentRow.user_id.is_(None), - AgentRow.user_id == user_id, - AgentRow.published.is_(True), - AgentRow.is_private.is_(True), - AgentRow.id.in_(fav_subq), - ) + """ + valid_favorite_exists = ( + select(AgentFavoriteRow.id) + .where( + AgentFavoriteRow.user_id == user_id, + AgentFavoriteRow.agent_id == AgentRow.id, + AgentRepository._valid_favorite_filter(), + ) + .exists() + ) + return or_( + AgentRow.user_id.is_(None), + AgentRow.user_id == user_id, + AgentRow.published.is_(True), + AgentRow.is_private.is_(True), + valid_favorite_exists, + ) @staticmethod def _listing_filter(user_id: str): @@ -268,11 +301,20 @@ class AgentRepository(AgentStore): or_(AgentRow.user_id.is_(None), AgentRow.published.is_(True)), AgentRow.is_private.is_(False), ) - if scope == "mine": - # owned OR favorited. A correlated IN-subquery (not a JOIN) keeps a - # single row per agent even when it is both owned and favorited. - fav_subq = select(AgentFavoriteRow.agent_id).where(AgentFavoriteRow.user_id == user_id) - return or_(AgentRow.user_id == user_id, AgentRow.id.in_(fav_subq)) + if scope == "mine": + # owned OR valid favorites. Built-in agents and business-chain + # seats only enter "mine" when explicitly favorited by the user; + # the original position-granted behavior stays for ordinary agents. + valid_favorite_exists = ( + select(AgentFavoriteRow.id) + .where( + AgentFavoriteRow.user_id == user_id, + AgentFavoriteRow.agent_id == AgentRow.id, + self._valid_favorite_filter(), + ) + .exists() + ) + return or_(AgentRow.user_id == user_id, valid_favorite_exists) if scope == "private": return AgentRow.is_private.is_(True) if scope == "builtin": @@ -437,18 +479,22 @@ class AgentRepository(AgentStore): # --- Favorites ("收藏") --- - async def add_favorite(self, user_id: str, agent_id: str) -> None: - async with self._sf() as session: - existing = ( - await session.execute( - select(AgentFavoriteRow.id) - .where(AgentFavoriteRow.user_id == user_id, AgentFavoriteRow.agent_id == agent_id) - .limit(1) - ) - ).scalar_one_or_none() - if existing is not None: - return - session.add(AgentFavoriteRow(id=uuid4().hex, user_id=user_id, agent_id=agent_id)) + async def add_favorite(self, user_id: str, agent_id: str) -> None: + async with self._sf() as session: + existing = ( + await session.execute( + select(AgentFavoriteRow) + .where(AgentFavoriteRow.user_id == user_id, AgentFavoriteRow.agent_id == agent_id) + .limit(1) + ) + ).scalar_one_or_none() + if existing is not None: + if existing.origin != "user" or existing.position_id is not None: + existing.origin = "user" + existing.position_id = None + await session.commit() + return + session.add(AgentFavoriteRow(id=uuid4().hex, user_id=user_id, agent_id=agent_id)) try: await session.commit() except IntegrityError: @@ -467,12 +513,17 @@ class AgentRepository(AgentStore): await session.commit() return (result.rowcount or 0) > 0 - async def list_favorite_ids(self, user_id: str) -> set[str]: - async with self._sf() as session: - result = await session.execute( - select(AgentFavoriteRow.agent_id).where(AgentFavoriteRow.user_id == user_id) - ) - return {row[0] for row in result.all()} + async def list_favorite_ids(self, user_id: str) -> set[str]: + async with self._sf() as session: + result = await session.execute( + select(AgentFavoriteRow.agent_id) + .join(AgentRow, AgentRow.id == AgentFavoriteRow.agent_id) + .where( + AgentFavoriteRow.user_id == user_id, + self._valid_favorite_filter(), + ) + ) + return {row[0] for row in result.all()} async def delete_favorites_for_agent(self, agent_id: str) -> None: async with self._sf() as session: diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260701_01_agent_position_favorites.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260701_01_agent_position_favorites.py new file mode 100644 index 00000000..264a12bf --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260701_01_agent_position_favorites.py @@ -0,0 +1,90 @@ +"""Stop position-granted favorites from collecting built-ins/chain seats. + +Built-in agents and agents used as business-chain seats should only enter +"mine" when a user explicitly clicks the favorite action. This migration +removes historical automatic ``origin='position'`` rows for those agents while +preserving user-created favorites (``origin='user'``) and ordinary +position-granted agent favorites. + +Revision ID: 20260701_01 +Revises: 20260625_04 +Create Date: 2026-07-01 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect + +revision = "20260701_01" +down_revision = "20260625_04" +branch_labels = None +depends_on = None + + +def _has_table(table_name: str) -> bool: + inspector = inspect(op.get_bind()) + return table_name in inspector.get_table_names() + + +def _has_column(table_name: str, column_name: str) -> bool: + if not _has_table(table_name): + return False + inspector = inspect(op.get_bind()) + return any(col["name"] == column_name for col in inspector.get_columns(table_name)) + + +def upgrade() -> None: + if not ( + _has_table("agent_favorites") + and _has_column("agent_favorites", "origin") + and _has_column("agent_favorites", "agent_id") + ): + return + + favorites = sa.table( + "agent_favorites", + sa.column("agent_id", sa.String(64)), + sa.column("origin", sa.String(16)), + ) + cleanup_conditions = [] + if _has_table("agents") and _has_column("agents", "user_id"): + agents = sa.table( + "agents", + sa.column("id", sa.String(64)), + sa.column("user_id", sa.String(64)), + ) + cleanup_conditions.append( + favorites.c.agent_id.in_(sa.select(agents.c.id).where(agents.c.user_id.is_(None))) + ) + if _has_table("roundtable_chains") and _has_column("roundtable_chains", "seats"): + chains = sa.table( + "roundtable_chains", + sa.column("seats", sa.Text()), + ) + cleanup_conditions.append( + sa.select(sa.literal(1)) + .select_from(chains) + .where( + sa.or_( + chains.c.seats.like(sa.literal('%"agent_id": "') + favorites.c.agent_id + sa.literal('"%')), + chains.c.seats.like(sa.literal('%"agent_id":"') + favorites.c.agent_id + sa.literal('"%')), + ) + ) + .exists() + ) + if not cleanup_conditions: + return + + op.get_bind().execute( + favorites.delete().where( + favorites.c.origin == "position", + sa.or_(*cleanup_conditions), + ) + ) + + +def downgrade() -> None: + # Data cleanup only; removed auto-grants are not reconstructable. + pass diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/template_fill.py b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/template_fill.py index 18ca995e..015933b4 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/template_fill.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/template_fill.py @@ -267,6 +267,69 @@ TEMPLATES: tuple[TemplateSpec, ...] = ( file="no18/no18.html", convertible=False, ), + TemplateSpec( + id="no19", + name="no.19", + description="", + file="no19/no.19.html", + convertible=False, + ), + TemplateSpec( + id="no20", + name="no.20", + description="", + file="no20/no.20.html", + convertible=False, + ), + TemplateSpec( + id="no21", + name="no.21", + description="", + file="no21/no.21.html", + convertible=False, + ), + TemplateSpec( + id="no22", + name="no.22", + description="", + file="no22/no.22.html", + convertible=False, + ), + TemplateSpec( + id="no23", + name="no.23", + description="", + file="no23/no.23.html", + convertible=False, + ), + TemplateSpec( + id="no24", + name="no.24", + description="", + file="no24/no.24.html", + convertible=False, + ), + TemplateSpec( + id="no25", + name="no.25", + description="", + file="no25/no.25.html", + convertible=False, + ), + TemplateSpec( + id="no26", + name="no.26", + description="", + file="no26/no.26.html", + convertible=False, + ), + TemplateSpec( + id="no27", + name="no.27", + description="", + file="no27/no.27.html", + convertible=False, + ), ) _TEMPLATES_BY_ID: dict[str, TemplateSpec] = {t.id: t for t in TEMPLATES} diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no19/no.19.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no19/no.19.html new file mode 100644 index 00000000..a7026b10 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no19/no.19.html @@ -0,0 +1,1764 @@ + + + + + +面对美国方向的“解放军反介入能力强大”叙事专题库 + + + +
公开资料研究看板 · PLA A2/AD Narrative

面对美国方向的“解放军反介入能力强大”叙事专题库

能力认知、美国政策转化、竞争叙事与反弹风险展示系统。本页面围绕“能力事实—美方认知—政策转化—竞争叙事—反弹风险—指标验证”展开,支持多 Tab 切换、关键词查询和二级弹窗阅读。
版本:1.0更新:2026-06-30边界:公开信息研究 / 政策分析 / 风险识别
+
+ +
本页面为专题研究展示模板。社交平台内容仅代表样本观察,不能直接代表整体民意;军事能力判断需交叉验证并保留不确定性。
+ + + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no20/no.20.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no20/no.20.html new file mode 100644 index 00000000..ca9016e6 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no20/no.20.html @@ -0,0 +1,4252 @@ + + + + + +日本反修宪、反战护宪与左翼市民社会生态资料库 + + + +
+
+

+

+
+
+ +
+ +
+
+ + + + + +
+
+
+
+ + +
+ + + + + + + diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no21/no.21.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no21/no.21.html new file mode 100644 index 00000000..2d9a127e --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no21/no.21.html @@ -0,0 +1,3833 @@ + + + + + + 日本左翼在野党涉华涉台政治生态知识库 + + + +
+
+
+
Interactive Knowledge Base
+

+

+
+
+
+

核心判断

+

+
+
+ +
+ +
+
+ + + + +
+
+
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no22/no.22.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no22/no.22.html new file mode 100644 index 00000000..c35040be --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no22/no.22.html @@ -0,0 +1,3484 @@ + + + + + +日本西南一线自卫队及关联群体生态数据库 + + + +
+
+ 公开资料研究 · 日本西南一线自卫队及关联群体 +

日本西南一线自卫队及关联群体生态数据库

+
本页以“自卫队西南一线部署链”为核心对象,串联陆上自卫队、航空自卫队、海上自卫队、海上保安厅、驻日美军、地方政府、居民、反基地网络、支持部署群体、媒体舆论、民防疏散和风险触发机制。
+
+
核心对象

第15旅团/未来第15师团、与那国沿岸监视队、宫古警备队、八重山警备队、奄美警备队、西南航空方面队、第5航空群和第11管区海保。

+
关联群体

地方政府、居民、自卫队家属、渔民、旅游业者、商户、反基地和平网络、支持部署保守派和媒体平台。

+
研究边界

只做公开资料层级的组织生态、地方社会、舆论叙事和风险沟通研究,不展示敏感军事细节。

+
+
+ +
+
+ + + + +
+
+
+ +
+ +
+ + + + + + + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no23/no.23.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no23/no.23.html new file mode 100644 index 00000000..4650be67 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no23/no.23.html @@ -0,0 +1,2943 @@ + + + + + +日本涉台叙事专题数据库 + + + +
+
JAPAN · TAIWAN NARRATIVE MONITORING
+

日本涉台叙事专题数据库: “台湾有事不是日本有事”

+

基于 Skill 与16栏专题知识库整理的可查询展示页面。支持多Tab切换、全局搜索、条件筛选、中文详情弹框和风险评分展示。

+ +
+

核心用途

用于长期监测日本社会中“台湾有事不是日本有事”及其竞争叙事的传播、受众、平台、政策触发、地方风险和趋势变化。

+
当前:中高观察 · 27/40

主叙事在冲绳、反战护宪、生活成本和中间选民领域存在扩散空间,但受到威慑和同盟叙事持续压制。

+
+
+
+ +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+ + + + + diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no24/no.24.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no24/no.24.html new file mode 100644 index 00000000..fb063b71 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no24/no.24.html @@ -0,0 +1,381 @@ + + + + + + 日本涉台介入风险叙事专题研究系统 + + + +
+
+
NARRATIVE RISK MONITORING DATABASE
+

日本涉台介入风险叙事专题研究系统

+
围绕“介入战争日本祸及自身”叙事的事件、人物、组织、制度、地方、媒体、经济与民意数据库
+
+ 版本:1.0 + 更新:2026-06-30 + 离线可用 · 内嵌数据 + Tab切换 · 搜索筛选 · 弹框详情 +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no25/no.25.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no25/no.25.html new file mode 100644 index 00000000..f8d40f41 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no25/no.25.html @@ -0,0 +1,3623 @@ + + + + + +菲律宾PDP–Laban与杜特尔特派支持者生态研究数据库(增强展示版) + + + +

增强版说明:弹框已改为卡片化详情页,不再直接展示原始JSON。页面内嵌增强版数据库,也支持导入同结构JSON更新展示。所有“待补/需核验”字段保留为研究缺口提醒。
+ + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no26/no.26.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no26/no.26.html new file mode 100644 index 00000000..243aed82 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no26/no.26.html @@ -0,0 +1,1432 @@ + + + + + +菲律宾海警一线生态知识展示平台|最终修正版 + + + +
+
+
+

菲律宾海警一线生态知识展示平台

+

最终修正版|已修复群体名称显示 · 群体栏目增强 · 复杂字段弹框展示 · 静态内容兜底

+
+ +
+
+
+
群体样本
10
类|已修复群体名称显示
事件记录
9
条|南海/西菲律宾海事件链
任务场景
6
类|巡逻、补给、护渔、搜救
装备后勤
6
项|舰艇、航空、无人系统、维护
叙事样本
12
类|媒体叙事与信息战样本
行动网络
5
张|PCG与外部节点互动链
来源审查
35
项|法律、预算、媒体、官方来源
+
+
+
+

总览

核心指标、运行链条、一线稳定性模型、核心事件和群体快览。

+
0条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+
+

PCG一线生态运行链条

海上摩擦→PCG记录→媒体传播→民意升温→外交抗议→盟友支持→预算和装备增加→PCG再次前出
+

一线稳定性模型

稳定性 = 风险可控感 + 组织信任感 + 家属支持感 + 社会尊重感 + 福利公平感

+
+
风险可控感

一线人员是否相信装备、训练、救援和指挥机制能控制任务风险

组织信任感

基层是否相信组织会兑现津贴、轮换、医疗和荣誉承诺

家属支持感

家庭能否承受长期分离、安全焦虑和媒体曝光带来的压力

社会尊重感

国内社会是否将PCG视为守护者,而不是单纯消耗品

福利公平感

津贴、晋升、表彰、医疗和退休是否被认为公平可靠

+
+

核心事件快览

点击查看弹框详情

+ 2024-06-17 +

仁爱礁补给任务冲突

+

菲律宾对BRP Sierra Madre驻守人员的补给任务遭遇中国海警强力拦截,菲律宾方面称出现人员受伤和设备损毁。

+
补给拦截碰撞人员受伤
+
事件过程
  • 菲律宾执行补给任务
  • 中国海警实施拦截和管控
  • 现场发生冲突
  • 菲律宾公开影像和说法
结果影响
  • 人员受伤
  • 菲律宾国内舆论升温
  • 美国及盟友支持菲律宾
  • PCG透明化叙事强化
+
透明化叙事主权维护叙事中国依法管控叙事菲律宾挑衅叙事
+
+
+ 2024-02-22 +

黄岩岛BFAR巡逻与护渔争议

+

菲律宾方面称BFAR船只在黄岩岛附近巡逻是为了保护菲律宾渔民,并反驳中方所谓非法进入说法。

+
护渔巡逻舆论争议
+
事件过程
  • BFAR船只在黄岩岛附近巡逻
  • 中方提出非法进入说法
  • PCG公开反驳
  • 事件进入媒体报道
结果影响
  • 护渔叙事强化
  • 渔民安全议题升温
  • 中菲官方口径分歧扩大
+
护渔民生叙事主权维护叙事中国依法管控叙事
+
+
+ 2024-04至2024-09 +

仙宾礁BRP Teresa Magbanua长期驻留

+

菲律宾将BRP Teresa Magbanua部署至仙宾礁,用于监视和维持存在,后续返回港口并宣称将派替代船只。

+
长期驻留巡逻拦截碰撞
+
事件过程
  • 菲律宾部署大型PCG船只
  • 双方围绕驻留和管控发生紧张
  • 出现封锁和碰撞指控
  • 菲律宾船只返回港口
结果影响
  • PCG从任务型出动转向持续存在型前沿力量
  • 后勤、补给、人员疲劳问题显性化
+
透明化叙事主权维护叙事中国依法管控叙事
+
+
+ 2026-04-09 +

Pag-asa启用Coast Guard District Kalayaan Island Group

+

菲律宾在Pag-asa岛激活新的海警区指挥节点,被地方官员视为提升居民士气的重要动作。

+
基地启用巡逻地方社会
+
事件过程
  • PCG建立或激活KIG指挥节点
  • 地方政府和居民公开表达安全期待
  • 媒体报道前沿居民长期面对周边船只存在
结果影响
  • PCG前沿化和基地化趋势加强
  • 地方社会与海警存在更深绑定
  • 居民安全感与战争风险感并存
+
主权维护叙事地方安全叙事英雄化叙事
+
+
+ 2025-06-05 +

BFAR KBBM项目支援WPS渔民

+

BFAR推出Kadiwa ng Bagong Bayaning Mangingisda项目,通过海上收购、燃油和冰补贴支持接近1000名WPS渔民。

+
护渔补给民生支援
+
事件过程
  • 政府组织海上收购与补贴
  • 渔民获得燃油与冰支持
  • PCG在安全与支援环节发挥协同作用
结果影响
  • 渔民生计与国家海上存在绑定
  • 民生叙事强化
  • 护渔任务制度化
+
护渔民生叙事主权维护叙事
+
+
+

群体名称快览

已直接显示群体名称、类型、任务、压力、稳定因素

+
群体名称
+

PCG高级指挥层

高级军官
+

负责组织扩编、对外沟通、战略协调、预算争取和透明化政策执行。

+
战略沟通政策执行组织扩编盟友协调预算争取
+
主要活动区域
  • Manila
  • DOTr体系
  • NTF-WPS
  • 外交与安全政策场域
压力来源
  • 对华摩擦升级压力
  • 国内问责压力
  • 盟友期待
  • 预算与装备缺口
  • 媒体透明化压力
+
稳定因素
  • 制度授权
  • 政治支持
  • 预算稳定
  • 盟友能力建设
  • 国内民意支持
关联事件
  • evt_renai_2024_06_17
  • evt_pagasa_2026_04
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

PCG基层一线人员

基层一线人员
+

承担海上巡逻、补给支援、护渔、搜救、证据记录等任务的一线制服人员。

+
海上巡逻护渔支援搜救补给支援证据记录海上执法
+
主要活动区域
  • Palawan
  • Kalayaan
  • 仁爱礁方向
  • 黄岩岛方向
  • 仙宾礁方向
压力来源
  • 长期海上执勤
  • 海上摩擦风险
  • 装备和补给压力
  • 媒体曝光压力
  • 家属安全焦虑
+
稳定因素
  • 风险可控感
  • 组织信任感
  • 家属支持感
  • 社会尊重感
  • 福利公平感
关联事件
  • evt_renai_2024_06_17
  • evt_sabina_2024_04_09
  • evt_pagasa_2026_04
+
任务数量6项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

士官骨干与舰艇船员

舰艇船员
+

负责船艇日常运行、海上安全、技术操作、基层纪律与经验传承。

+
船艇运行海上执勤通信值守设备维护损管与消防
+
主要活动区域
  • 大型MRRV
  • 巡逻艇
  • 前沿港口
  • 远海巡逻海域
压力来源
  • 长航时疲劳
  • 备件与维修压力
  • 狭小空间生活
  • 突发摩擦
  • 轮换公平
+
稳定因素
  • 专业荣誉
  • 技能晋升
  • 安全训练
  • 维修保障
  • 津贴兑现
关联事件
  • evt_sabina_2024_04_09
  • evt_tri_coastguard_2025
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

PCG航空与无人系统人员

航空人员
+

承担搜救、监视、灾害响应、海上态势感知和影像记录等任务。

+
空中搜救海上监视灾害响应无人机操作影像证据采集
+
主要活动区域
  • 海上巡逻区
  • 灾害响应区
  • 前沿港口
  • 训练中心
压力来源
  • 天气风险
  • 设备维护
  • 训练需求
  • 数据核验
  • 任务响应时效
+
稳定因素
  • 训练质量
  • 设备可靠性
  • 跨域协同
  • 技术支持
关联事件
  • evt_australia_drone_2025
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
+
+
+
+

群体分类

已加强:每张卡片显式展示群体名称、群体类型、任务、区域、压力、稳定因素和关联事件。

+
10条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+
群体名称
+

PCG高级指挥层

高级军官
+

负责组织扩编、对外沟通、战略协调、预算争取和透明化政策执行。

+
战略沟通政策执行组织扩编盟友协调预算争取
+
主要活动区域
  • Manila
  • DOTr体系
  • NTF-WPS
  • 外交与安全政策场域
压力来源
  • 对华摩擦升级压力
  • 国内问责压力
  • 盟友期待
  • 预算与装备缺口
  • 媒体透明化压力
+
稳定因素
  • 制度授权
  • 政治支持
  • 预算稳定
  • 盟友能力建设
  • 国内民意支持
关联事件
  • evt_renai_2024_06_17
  • evt_pagasa_2026_04
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

PCG基层一线人员

基层一线人员
+

承担海上巡逻、补给支援、护渔、搜救、证据记录等任务的一线制服人员。

+
海上巡逻护渔支援搜救补给支援证据记录海上执法
+
主要活动区域
  • Palawan
  • Kalayaan
  • 仁爱礁方向
  • 黄岩岛方向
  • 仙宾礁方向
压力来源
  • 长期海上执勤
  • 海上摩擦风险
  • 装备和补给压力
  • 媒体曝光压力
  • 家属安全焦虑
+
稳定因素
  • 风险可控感
  • 组织信任感
  • 家属支持感
  • 社会尊重感
  • 福利公平感
关联事件
  • evt_renai_2024_06_17
  • evt_sabina_2024_04_09
  • evt_pagasa_2026_04
+
任务数量6项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

士官骨干与舰艇船员

舰艇船员
+

负责船艇日常运行、海上安全、技术操作、基层纪律与经验传承。

+
船艇运行海上执勤通信值守设备维护损管与消防
+
主要活动区域
  • 大型MRRV
  • 巡逻艇
  • 前沿港口
  • 远海巡逻海域
压力来源
  • 长航时疲劳
  • 备件与维修压力
  • 狭小空间生活
  • 突发摩擦
  • 轮换公平
+
稳定因素
  • 专业荣誉
  • 技能晋升
  • 安全训练
  • 维修保障
  • 津贴兑现
关联事件
  • evt_sabina_2024_04_09
  • evt_tri_coastguard_2025
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

PCG航空与无人系统人员

航空人员
+

承担搜救、监视、灾害响应、海上态势感知和影像记录等任务。

+
空中搜救海上监视灾害响应无人机操作影像证据采集
+
主要活动区域
  • 海上巡逻区
  • 灾害响应区
  • 前沿港口
  • 训练中心
压力来源
  • 天气风险
  • 设备维护
  • 训练需求
  • 数据核验
  • 任务响应时效
+
稳定因素
  • 训练质量
  • 设备可靠性
  • 跨域协同
  • 技术支持
关联事件
  • evt_australia_drone_2025
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

维修与后勤人员

维修与后勤人员
+

支撑舰艇、快艇、直升机、无人系统和前沿行动的维修、备件、燃油、医疗、饮水与补给。

+
维修保障备件管理燃油补给港口支撑医疗与饮水保障
+
主要活动区域
  • 港口
  • 维修点
  • 前沿补给节点
  • 训练基地
压力来源
  • 装备数量上升
  • 远海任务增加
  • 备件周期
  • 前沿维修能力
  • 灾害任务叠加
+
稳定因素
  • 技术合作
  • SOP建设
  • 维修训练
  • 预算稳定
  • 供应链可靠
关联事件
  • evt_jica_maintenance_2025
  • evt_pcg_foundation_water
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

PCG人员家属

家属
+

一线人员稳定性的重要后方支撑,关切安全、收入、医疗、教育、探亲和隐私保护。

+
情感支持家庭照护长期分离承受安全焦虑管理
+
主要活动区域
  • 家庭社区
  • PCG Foundation项目
  • 地方家属支持网络
压力来源
  • 安全焦虑
  • 长期分离
  • 信息不透明
  • 子女教育
  • 医疗康复
+
稳定因素
  • 福利兑现
  • 医疗支持
  • 心理健康项目
  • 家庭日活动
  • 隐私保护
关联事件
  • evt_pcg_family_day_2025
  • evt_pcg_health_2025
+
任务数量4项
压力因素6项
安全级别敏感群体,仅群体级
+
+
+
群体名称
+

西菲律宾海渔民

渔民
+

既是民生主体,也是菲律宾海上存在、护渔叙事和地方社会安全感的重要来源。

+
捕鱼作业海上民生存在接受补给提供地方经验表达安全诉求
+
主要活动区域
  • 黄岩岛附近
  • 仙宾礁附近
  • Zambales
  • Palawan
  • Bataraza
压力来源
  • 水炮与拦截风险
  • 禁航或避让导致收入下降
  • 燃油成本
  • 冰和市场损耗
  • 军演外溢影响
+
稳定因素
  • BFAR补贴
  • PCG护航
  • 地方政府支持
  • 安全通报
  • 海上收购项目
关联事件
  • evt_bfar_kbbm_2025
  • evt_sabina_fishermen_2025
  • evt_scarborough_resupply_2025
+
任务数量5项
压力因素5项
安全级别群体级
+
+
+
群体名称
+

Pag-asa / Thitu前沿居民

地方居民
+

生活在南沙前沿社区的平民居民,其安全感、生计、教育、通信和补给状况与PCG存在密切相关。

+
社区生活渔业生计地方公共服务维持平民存在
+
主要活动区域
  • Pag-asa Island
  • Kalayaan
  • Palawan
压力来源
  • 周边船只存在
  • 通信和漫游争议
  • 补给距离远
  • 儿童教育
  • 安全焦虑
+
稳定因素
  • 学校与诊所
  • 港口和跑道
  • PCG指挥节点
  • 地方政府补贴
  • 社区韧性
关联事件
  • evt_pagasa_2026_04
  • evt_pagasa_residents_2024
+
任务数量4项
压力因素5项
安全级别社区级
+
+
+
群体名称
+

菲律宾媒体与透明化传播者

媒体
+

通过现场影像、报道、采访和事实核查,将海上事件转化为国内外可理解的政治与舆论材料。

+
事件报道现场视频传播事实核查叙事放大舆论监督
+
主要活动区域
  • 新闻平台
  • 社交媒体
  • 媒体随船
  • 校园宣讲
  • 事实核查平台
压力来源
  • 信息战
  • 假信息
  • 政治攻击
  • 民族主义情绪
  • 外交敏感性
+
稳定因素
  • 来源核验
  • 透明化政策
  • 多方对照
  • 事实核查机构
关联事件
  • evt_transparency_pulse_2025
  • evt_tarriela_embassy_2026
+
任务数量5项
压力因素5项
安全级别公开传播级
+
+
+
群体名称
+

美国、日本、澳大利亚等外部伙伴

外部盟友
+

通过装备、训练、无人系统、联合演训、外交背书和能力建设支持PCG现代化。

+
装备援助训练合作联合演习技术合作外交支持态势感知建设
+
主要活动区域
  • 美国本土课程
  • 日本海上保安厅合作
  • 澳大利亚无人机项目
  • 菲律宾训练中心
压力来源
  • 中方反制叙事
  • 反美军事化批评
  • 地方社区战争风险感
  • 合作可持续性
+
稳定因素
  • 资金项目
  • 制度合作
  • 训练SOP
  • 多边演训
  • 外交支持
关联事件
  • evt_us_training_2025
  • evt_jica_mrrv_2024
  • evt_australia_drone_2025
  • evt_tri_coastguard_2025
+
任务数量6项
压力因素4项
安全级别机构级
+
+
+
+
+

任务场景

巡逻、补给、护渔、搜救、证据记录、联合训练等任务。

+
6条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

海上巡逻与存在展示

巡逻
+

PCG通过白船巡逻、无线电通联、现场影像和公开发声展示菲律宾在争议海域的执法存在。

+
参与群体
  • PCG基层一线人员
  • 舰艇船员
  • 媒体
  • NTF-WPS
典型地点
  • 仁爱礁
  • 黄岩岛
  • 仙宾礁
  • 中业岛
  • Palawan
操作要素
  • 巡逻
  • 记录
  • 通信
  • 执法存在
  • 媒体发布
社会意义

将海上执法行为转化为主权、规则秩序和国内政治叙事。

+
+
+

补给与轮换支援

补给
+

围绕驻守点、渔民和前沿社区开展补给、轮换、护航和医疗支援。

+
参与群体
  • PCG基层一线人员
  • 菲律宾军方
  • BFAR
  • 渔民
  • 地方政府
典型地点
  • 仁爱礁
  • Pag-asa Island
  • 黄岩岛
  • 仙宾礁
操作要素
  • 食品补给
  • 燃油补给
  • 医疗救助
  • 轮换支援
  • 影像记录
社会意义

补给任务被政治化和国际化,常成为主权争议和外交抗议触发点。

+
+
+

护渔与渔民补给

护渔
+

PCG与BFAR协同,为争议海域附近菲律宾渔民提供护航、补给、医疗和信息支持。

+
参与群体
  • PCG基层一线人员
  • BFAR
  • 菲律宾渔民
  • 地方政府
  • 媒体
典型地点
  • 黄岩岛
  • 仙宾礁
  • Palawan
  • Zambales
操作要素
  • 燃油补给
  • 食品补给
  • 医疗救助
  • 海上陪伴
  • 影像记录
社会意义

护渔任务将PCG执法角色与普通渔民生计安全绑定。

+
+
+

海上搜救

搜救
+

PCG传统核心职责,负责遇险人员和船舶搜索、救援和医疗转送。

+
参与群体
  • PCG基层一线人员
  • 航空人员
  • 医疗人员
  • 地方政府
典型地点
  • 菲律宾群岛周边海域
  • 灾害影响区域
  • 航道和港口
操作要素
  • 搜寻
  • 救援
  • 医疗转送
  • 灾害响应
  • 应急协调
社会意义

搜救构成PCG最稳定的社会合法性来源。

+
+
+

证据记录与透明化发布

证据记录
+

通过视频、照片、无线电记录、媒体随船和官方说明,把海上事件转化为可传播证据。

+
参与群体
  • PCG
  • NTF-WPS
  • 媒体
  • 事实核查机构
  • 外交部
典型地点
  • 海上摩擦现场
  • 新闻发布会
  • 社交平台
  • 校园宣讲
操作要素
  • 影像采集
  • 事件核验
  • 新闻发布
  • 事实核查
  • 国际传播
社会意义

透明化被菲律宾视为反制虚假叙事和争取国际支持的重要工具。

+
+
+

联合训练与能力建设

联合演训
+

与美国、日本、澳大利亚、越南等开展海警训练、搜救、消防、通信、无人系统和法证能力建设。

+
参与群体
  • PCG
  • USCG
  • JCG
  • 澳大利亚
  • 越南海警
典型地点
  • 日本鹿儿岛
  • 菲律宾训练中心
  • 美国课程
  • 越南合作场景
操作要素
  • 搜救演练
  • 通信演练
  • 消防演练
  • 无人机训练
  • 维护SOP
社会意义

PCG正在进入区域海警合作网络。

+
+
+
+
+

南海事件库

仁爱礁、黄岩岛、仙宾礁、Pag-asa等事件时间线。

+
9条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+ 2024-06-17 +

仁爱礁补给任务冲突

+

菲律宾对BRP Sierra Madre驻守人员的补给任务遭遇中国海警强力拦截,菲律宾方面称出现人员受伤和设备损毁。

+
补给拦截碰撞人员受伤
+
事件过程
  • 菲律宾执行补给任务
  • 中国海警实施拦截和管控
  • 现场发生冲突
  • 菲律宾公开影像和说法
结果影响
  • 人员受伤
  • 菲律宾国内舆论升温
  • 美国及盟友支持菲律宾
  • PCG透明化叙事强化
+
透明化叙事主权维护叙事中国依法管控叙事菲律宾挑衅叙事
+
+
+ 2024-02-22 +

黄岩岛BFAR巡逻与护渔争议

+

菲律宾方面称BFAR船只在黄岩岛附近巡逻是为了保护菲律宾渔民,并反驳中方所谓非法进入说法。

+
护渔巡逻舆论争议
+
事件过程
  • BFAR船只在黄岩岛附近巡逻
  • 中方提出非法进入说法
  • PCG公开反驳
  • 事件进入媒体报道
结果影响
  • 护渔叙事强化
  • 渔民安全议题升温
  • 中菲官方口径分歧扩大
+
护渔民生叙事主权维护叙事中国依法管控叙事
+
+
+ 2024-04至2024-09 +

仙宾礁BRP Teresa Magbanua长期驻留

+

菲律宾将BRP Teresa Magbanua部署至仙宾礁,用于监视和维持存在,后续返回港口并宣称将派替代船只。

+
长期驻留巡逻拦截碰撞
+
事件过程
  • 菲律宾部署大型PCG船只
  • 双方围绕驻留和管控发生紧张
  • 出现封锁和碰撞指控
  • 菲律宾船只返回港口
结果影响
  • PCG从任务型出动转向持续存在型前沿力量
  • 后勤、补给、人员疲劳问题显性化
+
透明化叙事主权维护叙事中国依法管控叙事
+
+
+ 2026-04-09 +

Pag-asa启用Coast Guard District Kalayaan Island Group

+

菲律宾在Pag-asa岛激活新的海警区指挥节点,被地方官员视为提升居民士气的重要动作。

+
基地启用巡逻地方社会
+
事件过程
  • PCG建立或激活KIG指挥节点
  • 地方政府和居民公开表达安全期待
  • 媒体报道前沿居民长期面对周边船只存在
结果影响
  • PCG前沿化和基地化趋势加强
  • 地方社会与海警存在更深绑定
  • 居民安全感与战争风险感并存
+
主权维护叙事地方安全叙事英雄化叙事
+
+
+ 2025-06-05 +

BFAR KBBM项目支援WPS渔民

+

BFAR推出Kadiwa ng Bagong Bayaning Mangingisda项目,通过海上收购、燃油和冰补贴支持接近1000名WPS渔民。

+
护渔补给民生支援
+
事件过程
  • 政府组织海上收购与补贴
  • 渔民获得燃油与冰支持
  • PCG在安全与支援环节发挥协同作用
结果影响
  • 渔民生计与国家海上存在绑定
  • 民生叙事强化
  • 护渔任务制度化
+
护渔民生叙事主权维护叙事
+
+
+ 2025-10 +

PCG与BFAR为黄岩岛和仙宾礁附近渔民补给

+

PCG与BFAR向约100艘菲律宾渔船提供燃油、食品和其他补给,以延长其在争议海域的作业时间。

+
护渔补给巡逻
+
事件过程
  • 菲律宾派出PCG和BFAR船只
  • 向渔民提供补给
  • 媒体报道中提及中方干扰背景
  • 护渔和民生叙事强化
结果影响
  • 护渔网络制度化
  • 渔民安全和生计议题继续升温
+
护渔民生叙事透明化叙事
+
+
+ 2025-12 +

仙宾礁附近渔民受伤与PCG救助

+

菲律宾方面称中国海警在仙宾礁附近使用水炮和阻挡动作,导致菲律宾渔民受伤、渔船受损,PCG随后提供医疗和物资支援。

+
水炮护渔人员受伤医疗救助
+
事件过程
  • 渔民在相关海域作业
  • 发生水炮和阻挡事件
  • 渔民受伤与船只受损
  • PCG抵达并提供救助
结果影响
  • PCG护渔合法性强化
  • 中菲叙事冲突加剧
  • 渔民民生风险凸显
+
护渔民生叙事透明化叙事中国依法管控叙事
+
+
+ 2025-06 +

美日菲三边海警演训

+

PCG参加美日菲三边海警演练,科目包括通信、搜救、消防、拍照和人员转移。

+
联合演训
+
事件过程
  • PCG派出代表和船只
  • 与JCG和USCG进行多科目演练
  • 强化互操作性和规则秩序叙事
结果影响
  • PCG区域海警网络深化
  • 能力建设提升
  • 反美或反军事化叙事可能升温
+
亲盟友叙事规则秩序叙事反美叙事
+
+
+ 2025-09 +

菲律宾民众支持WPS透明化政策

+

调查显示大量菲律宾民众支持政府公开披露中国对菲律宾船只和渔民的强制行为,透明化成为PCG叙事核心工具。

+
舆论争议
+
事件过程
  • 政府持续公开影像和事件信息
  • 民调机构采样
  • 媒体报道民意结果
  • 透明化政策获得政治资源
结果影响
  • PCG透明化叙事合法性增强
  • 国内对WPS议题关注升温
+
透明化叙事主权维护叙事事实核查叙事
+
+
+
+
+

装备与后勤

舰艇、航空、无人系统、维护合作与后勤压力。

+
6条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

97米级多用途响应船

大型巡逻舰艇
+

提升PCG在西菲律宾海方向的远海存在能力。

+
提供方

日本 / JICA

数量/规模

既有2艘,新增5艘项目

功能
  • 远海巡逻
  • 搜救
  • 海上执法
  • 持续存在
  • 证据记录
后勤压力
  • 维修需求上升
  • 备件保障
  • 港口支持
  • 人员训练
  • 长航时补给
+
+
+

44米级多用途响应船

中型巡逻船
+

构成PCG现代化舰队的中坚基础。

+
提供方

日本 / JICA

数量/规模

10艘,公开项目口径

功能
  • 近中海巡逻
  • 执法
  • 搜救
  • 海上安全
后勤压力
  • 维护训练
  • 港口支持
  • 备件采购
+
+
+

40艘快速巡逻艇

快速巡逻艇
+

增强PCG分布式快速反应能力,与大型MRRV形成互补。

+
提供方

法国 OCEA

数量/规模

40艘项目

功能
  • 快速反应
  • 近岸巡逻
  • 搜救
  • 海上执法
后勤压力
  • 分布式维护
  • 人员训练
  • 燃油和港口支持
+
+
+

Airbus H145直升机

航空资产
+

让PCG任务从海面延伸到空中支援和灾害响应。

+
提供方

Airbus

数量/规模

公开报道显示PCG成为菲律宾H145公共任务运营方

功能
  • 搜救
  • 灾害响应
  • 公共安全
  • 医疗转送
后勤压力
  • 航空维修
  • 飞行训练
  • 天气风险
  • 备件
+
+
+

澳大利亚无人机与无人海事态势感知技术

无人系统
+

提升PCG低成本感知能力和透明化材料生产能力。

+
提供方

澳大利亚

数量/规模

PHP 36M已交付,最高PHP 110M后续承诺

功能
  • 海上态势感知
  • 监视
  • 搜救
  • 影像记录
后勤压力
  • 操作训练
  • 数据处理
  • 设备维护
  • 通信链路
+
+
+

JICA 2025—2028运行维护与调查能力合作

维护与制度化项目
+

装备增加后的关键瓶颈从采购转向维护、训练和制度化。

+
提供方

JICA / PCG

数量/规模

2025—2028技术合作

功能
  • 维护能力
  • 调查能力
  • SOP制定
  • 教官培养
  • 制度化
后勤压力
  • 技术吸收
  • 流程标准化
  • 多地区复制
  • 长期资金
+
+
+
+
+

预算与福利

年度预算、项目拨款、津贴、补助、退休与后方保障。

+
2条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

2026年PCG公开预算

2026
+
总拨款PHP 42,514,531,000
人员经费PHP 26,441,518,000
MOOEPHP 8,953,720,000
资本支出PHP 7,119,293,000
+
主要项目
  • Maritime Security and Law Enforcement|PHP 16.248B
  • Maritime Search and Rescue|PHP 6.714B
  • Marine Environmental Protection|PHP 3.411B
  • Maritime Safety|PHP 3.407B
+
福利项目
  • Rice Subsidy:每月20公斤大米形式援助
  • Subsistence Allowance:制服人员口粮津贴
  • Quick Response Fund:灾害、危机和救援行动资金
+
分析价值

预算结构显示PCG正在从传统海事机构向高频执法和安全存在机构转型。

+
+
+

RA 9993薪酬、津贴与退休制度

长期制度
+
总拨款—
人员经费—
MOOE—
资本支出—
+
主要项目
  • Basic pay and longevity pay|制度性安排
  • Hazard pay and allowances|制度性安排
  • Retirement system|按服役年限和年龄适用
+
福利项目
  • 危险津贴
  • 退休制度
  • 医疗和伤亡相关保障需与年度预算核验
+
分析价值

薪酬、津贴和退休制度构成一线人员持续执行高风险任务的制度基础。

+
+
+
+
+

家属生态

家属支持、医疗、教育、心理韧性和安全限制。

+
3条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

PCG Foundation后方支持体系

PCG Foundation
+

家属支持体系是PCG一线稳定性的重要组成部分。

+
支持领域
  • 医疗
  • 教育
  • 生计
  • 福利
  • 救灾
  • 心理健康
压力类型
  • 安全焦虑
  • 长期分离
  • 信息不透明
  • 收入稳定需求
  • 子女教育压力
  • 医疗康复压力
公开项目
  • Family Day
  • Health and Wellness Caravan
  • 医疗与心理健康活动
安全限制
  • 不采集家属姓名
  • 不采集住址
  • 不采集社交账号
  • 不建立个人弱点画像
+
+
+

家庭日与组织价值叙事

PCG Foundation
+

PCG把家庭塑造成组织忠诚、服务精神和国家建设的一部分。

+
支持领域
  • 家庭价值
  • 组织认同
  • 后方支持
  • 服务精神
压力类型
  • 长期分离
  • 安全焦虑
  • 家庭照护压力
公开项目
  • 2025 Family Day
安全限制
  • 只分析公开活动与群体叙事
  • 不追踪个人家庭信息
+
+
+

健康与心理韧性项目

PCG Foundation / Coast Guard District Western Visayas
+

说明PCG已把身心健康和长期承压作为组织福利议题处理。

+
支持领域
  • 身体健康
  • 心理健康
  • 压力管理
  • 情绪韧性
  • 生计机会
压力类型
  • 任务疲劳
  • 家庭焦虑
  • 灾害响应压力
  • 长期海上执勤压力
公开项目
  • Health and Wellness Caravan
安全限制
  • 不记录个人健康信息
  • 仅做项目级分析
+
+
+
+
+

地方社会

Pag-asa、Kalayaan、Palawan、Zambales等地方社区和渔民支撑节点。

+
5条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

Pag-asa Island / Thitu Island

Kalayaan, Palawan
+

公开报道显示,当地居民和地方官员将PCG常设存在视为士气提升,但也长期面对中国船只存在和安全焦虑。

+
人口公开口径

约387—400名居民,公开报道口径

基础设施
  • 学校
  • 诊所
  • 港口
  • 跑道
  • 疏散中心
  • 小教堂
  • 商店
社会议题
  • 居民安全感
  • 渔民生计
  • 儿童教育
  • 补给稳定
  • 通信环境
  • 大国竞争风险
PCG存在

Coast Guard District Kalayaan Island Group

+
+
+

Kalayaan Municipality

Palawan
+

地方政府将PCG存在视为对居民、渔民和地方行政存在的重要支撑。

+
人口公开口径

公开资料显示其管辖Pag-asa及若干岛礁,具体人口需以地方统计核验

基础设施
  • 地方政府
  • 港口服务
  • 岛上基础设施
  • 前沿公共服务
社会议题
  • 远离本岛
  • 补给距离
  • 地方财政
  • 岛上教育
  • 安全存在
PCG存在

PCG KIG指挥节点与海警站

+
+
+

Bataraza / Buliluyan Port

Palawan
+

该地是将民生补贴、渔业市场和海上存在连接起来的关键节点。

+
人口公开口径

作为KBBM渔民支援项目节点处理

基础设施
  • 港口
  • 渔业补给节点
  • 海上收购支撑
社会议题
  • 渔民收入
  • 燃油与冰补贴
  • 市场损耗
  • WPS作业风险
PCG存在

PCG在渔民支援和护渔场景中提供安全协同

+
+
+

Zambales沿海渔民社区

Zambales
+

渔民既关注中国海警压力,也关注美菲军演和禁航区对生计的影响。

+
人口公开口径

不采集个人和村级敏感信息,仅作为渔民群体分析

基础设施
  • 渔港
  • 沿海社区
  • 渔民组织网络
社会议题
  • 黄岩岛作业受限
  • 渔民生计
  • 反军事化诉求
  • 军演禁航区影响
PCG存在

PCG护渔、救助和透明化叙事关联区域

+
+
+

Balabac方向

Palawan
+

该区域体现地方社会、PCG行动和盟友军事/安全存在之间的叠加影响。

+
人口公开口径

不采集地方居民个人数据

基础设施
  • EDCA相关公开地点
  • 前沿岛屿和海上通道
社会议题
  • 安全保障
  • 战争风险感
  • 地方经济
  • 反美/亲美分歧
  • 基地化外溢
PCG存在

PCG前沿生态受EDCA和美菲安全架构间接影响

+
+
+
+
+

舆论叙事

透明化、主权、护渔、英雄化、反美、反战、事实核查等叙事。

+
7条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

透明化叙事

官方叙事
+

通过公开现场影像、事件经过和官方说明,反制虚假信息并争取国内外支持。

+
主要行为体
  • PCG
  • NTF-WPS
  • 菲律宾政府
  • 主流媒体
传播渠道
  • 新闻发布会
  • 现场视频
  • 官方社交媒体
  • 媒体随船
  • 校园宣讲
正面效果
  • 提升国际关注
  • 增加国内支持
  • 强化PCG正面形象
  • 支持预算和装备扩充
风险效果
  • 舆论极化
  • 外交空间收窄
  • 前线人员符号化
反向叙事
  • 菲律宾挑衅叙事
  • 美国操纵叙事
  • 反军事化叙事
+
+
+

主权维护叙事

官方/民族主义叙事
+

菲律宾在西菲律宾海拥有主权权利,PCG行动是依法维护国家权益。

+
主要行为体
  • PCG
  • DFA
  • DND
  • 参议院主权派
  • 媒体
传播渠道
  • 官方声明
  • 参议院决议
  • 新闻报道
  • 民调传播
正面效果
  • 凝聚国内支持
  • 支撑外交抗议
  • 提升PCG英雄化形象
风险效果
  • 民族主义情绪升高
  • 对话空间收窄
反向叙事
  • 谨慎外交叙事
  • 反军事化叙事
+
+
+

护渔民生叙事

民生叙事
+

PCG与BFAR行动是为了保护普通渔民的生计、安全和尊严。

+
主要行为体
  • PCG
  • BFAR
  • 渔民
  • 地方政府
  • 媒体
传播渠道
  • 项目公告
  • 救助报道
  • 渔民采访
  • 地方媒体
正面效果
  • 强化PCG社会合法性
  • 把主权议题转化为民生议题
风险效果
  • 渔民被卷入主权竞争
  • 渔民安全风险升高
反向叙事
  • 反军事化民生叙事
+
+
+

反美叙事

批评/影响叙事
+

美国利用菲律宾对抗中国,EDCA和联合演习会使菲律宾成为战争前线。

+
主要行为体
  • 亲中信息网络
  • 部分杜特尔特式缓和派
  • 反战群体
  • 左翼民生组织
传播渠道
  • 社交媒体
  • 短视频
  • 意见文章
  • 伪媒体
  • 抗议声明
正面效果
  • 动员反战和地方社区安全关切
风险效果
  • 削弱PCG盟友能力建设合法性
  • 放大亲中或反美极化
反向叙事
  • 亲盟友叙事
  • 规则秩序叙事
+
+
+

反战与反军事化叙事

民生/左翼/人权叙事
+

菲律宾应保护渔民免受中国压力,同时避免被美菲军事化推向大国冲突前线。

+
主要行为体
  • APHR
  • PAMALAKAYA
  • 左翼组织
  • 沿海社区
传播渠道
  • 声明
  • 抗议
  • 媒体采访
  • 人权话语
正面效果
  • 提醒政策制定者关注渔民生计和地方社区风险
风险效果
  • 可能被主权强硬派批评为削弱对华立场
反向叙事
  • 主权维护叙事
  • 亲盟友叙事
+
+
+

中国依法管控叙事

中国官方叙事
+

中方在相关海域采取措施是依法管控,菲律宾船只非法进入或挑衅。

+
主要行为体
  • 中国海警
  • 中国外交部
  • 中国驻菲律宾使馆
  • 中国媒体
传播渠道
  • 官方声明
  • 使馆发言
  • 媒体报道
  • 社交平台
正面效果
  • 服务中国对内外解释和法理叙事
风险效果
  • 与菲律宾透明化叙事激烈冲突
  • 加深双方公众对立
反向叙事
  • 透明化叙事
  • 主权维护叙事
+
+
+

事实核查叙事

事实核查/媒体素养叙事
+

WPS争议中存在地图、法律概念、伪造发言、AI错误回答和历史叙事误读,需要事实核查。

+
主要行为体
  • VERA Files
  • 媒体
  • 学者
  • 透明化传播者
传播渠道
  • 事实核查文章
  • 解释性报道
  • 媒体素养活动
正面效果
  • 提升信息质量
  • 减少误导
  • 支持理性公共讨论
风险效果
  • 事实核查也可能被政治化攻击
反向叙事
  • 虚假信息
  • 阴谋论
  • 伪造内容
+
+
+
+
+

社交媒体与信息战

社交平台样本、错误叙事、事实核查和风险等级。

+
5条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

反美叙事样本

中
+

美国利用菲律宾对抗中国,EDCA和联合演习会使菲律宾成为战争前线。

+
目标受众
  • 普通菲律宾民众
  • 沿海社区
  • 反战群体
  • 杜特尔特支持者
  • 左翼民生组织
相关议题
  • EDCA基地
  • Balikatan演习
  • 渔民禁航区
  • 战争风险
  • 地方安全焦虑
反制叙事
  • 规则秩序叙事
  • 同盟威慑叙事
  • PCG能力建设叙事
核验状态

需结合事实核查机构和平台样本进一步核验

+
+
+

亲中缓和叙事样本

中
+

中国是重要发展伙伴,菲律宾应避免对抗并恢复对话。

+
目标受众
  • 经贸群体
  • 杜特尔特式缓和派
  • 战争风险敏感群体
相关议题
  • 中菲经贸
  • 外交降温
  • 美国介入
  • 海上摩擦
反制叙事
  • 主权优先叙事
  • 透明化叙事
核验状态

需区分真实缓和派意见与协调性影响行动

+
+
+

伪造政治人物发言样本

中高
+

通过伪造或断章取义政治人物发言影响WPS争议认知。

+
目标受众
  • 普通社交媒体用户
  • 政党支持者
  • 低信息关注群体
相关议题
  • WPS立场
  • 选举政治
  • 政治攻击
反制叙事
  • 事实核查叙事
  • 来源核验
核验状态

事实核查机构已有相关类型案例

+
+
+

地图标签误读样本

中
+

把Google Maps或AI回答中的WPS标注误读为法律结论。

+
目标受众
  • 普通用户
  • 学生
  • 社交平台讨论者
相关议题
  • 地图标签
  • 主权与主权权利
  • 法律概念
反制叙事
  • 法律解释
  • 事实核查
  • 媒体素养
核验状态

需通过事实核查和法律解释纠偏

+
+
+

反透明化叙事样本

中高
+

PCG公开影像和官员发言是在挑衅中国、制造紧张或服务美国。

+
目标受众
  • 谨慎外交派
  • 反美群体
  • 对战争风险敏感群体
相关议题
  • PCG发言人
  • 中国使馆争议
  • 外交降温
  • 媒体透明化
反制叙事
  • 透明化是反虚假信息工具
  • 公众知情权
  • 规则秩序
核验状态

需要对比官方声明、民调和事实核查

+
+
+
+
+

外部行动网络

PCG、BFAR、渔民、地方政府、媒体和盟友之间的行动链。

+
5条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

PCG-BFAR-渔民护渔网络

民生安全网络
+

民生安全网络

+
节点角色
  • PCG;护航、搜救、医疗、证据记录
  • BFAR;渔业管理、渔民支援、燃油和冰补贴
  • 菲律宾渔民;生计主体、海上存在、民生合法性来源
  • 地方政府;港口、社区、补给和地方政治支持
  • 媒体;传播渔民处境和PCG行动
互动链条
  • 渔民进入争议海域
  • BFAR提供民生支援
  • PCG提供护航和安全支持
  • 事件被媒体记录
  • 国内舆论形成压力
  • 政府扩大护渔与补给项目
关联事件
  • evt_bfar_kbbm_2025
  • evt_scarborough_resupply_2025
  • evt_sabina_fishermen_2025
+
+
+

PCG-NTF-WPS-媒体透明化网络

舆论与证据网络
+

舆论与证据网络

+
节点角色
  • PCG;现场记录、发言和影像公开
  • NTF-WPS;事件核验、跨部门协调和口径发布
  • DFA;外交抗议和国际法叙事
  • 媒体;报道和传播
  • 事实核查机构;纠错、核验和解释
互动链条
  • 海上事件发生
  • PCG记录影像
  • NTF-WPS核验口径
  • 媒体传播
  • DFA与盟友回应
  • 民意和政策反馈
关联事件
  • evt_transparency_pulse_2025
  • evt_tarriela_embassy_2026
  • evt_renai_2024_06_17
+
+
+

PCG-盟友训练与能力建设网络

外部能力建设网络
+

外部能力建设网络

+
节点角色
  • PCG;接收训练、装备和制度化能力
  • 美国/USCG;课程、训练、运行维护和教官培养
  • 日本/JCG/JICA;船艇、维护、海上执法和环保训练
  • 澳大利亚;无人系统、态势感知和训练
  • 越南海警;低敏感度区域协作
互动链条
  • 南海摩擦长期化
  • 菲律宾请求能力建设
  • 盟友提供装备训练
  • PCG能力提升
  • 中国和反美叙事回应
  • 菲律宾继续前出
关联事件
  • evt_us_training_2025
  • evt_jica_mrrv_2024
  • evt_tri_coastguard_2025
  • evt_vietnam_joint_2024
+
+
+

PCG Foundation后方支持网络

福利与韧性网络
+

福利与韧性网络

+
节点角色
  • PCG Foundation;医疗、教育、生计、福利、救灾项目
  • PCG人员;受益对象和服务主体
  • 家属;后方稳定和情感支持
  • 捐赠者/社会组织;物资和项目支持
互动链条
  • 一线任务压力增加
  • 人员与家属福利需求上升
  • 基金会组织健康、家庭和物资支持
  • 组织稳定性增强
关联事件
  • evt_pcg_family_day_2025
  • evt_pcg_health_2025
+
+
+

PCG-地方政府-前沿社区网络

地方社会网络
+

地方社会网络

+
节点角色
  • PCG;海上安全、搜救、常设存在
  • Kalayaan地方政府;地方行政、居民服务和政治表达
  • Pag-asa居民;平民存在、社区生活和安全感反馈
  • 军方;后台安全存在和岛上基础保障
  • 媒体;传播地方体验
互动链条
  • 前沿社区长期存在
  • 周边船只压力增加
  • PCG常设节点启用
  • 居民士气提升
  • 地方被卷入大国竞争叙事
关联事件
  • evt_pagasa_2026_04
  • evt_pagasa_residents_2024
+
+
+
+
+

趋势与来源

趋势研判、来源审查与安全规则。

+
40条记录主页面摘要弹框展示长字段
+
+
点击“查看完整详情”查看二级信息、来源和长字段;搜索框可全局过滤当前页面内容。
+
+

PCG扩编与前沿化

组织趋势
+

PCG人员规模、舰艇装备、前沿指挥节点和盟友训练合作显示其正在从传统海事机构转向前沿海上执法力量。

+
驱动因素
  • 南海摩擦长期化
  • 菲律宾透明化政策
  • 美国、日本、澳大利亚支持
  • 国内民意支持
  • 渔民保护需求
可能结果
  • 更多前沿部署节点
  • 更多大型巡逻船
  • 更多无人机和态势感知设备
  • 更强的媒体传播能力
  • 更高的一线人员福利需求
风险
  • 中菲摩擦升级
  • 地方社区安全焦虑
  • 反美和反军事化叙事升温
  • PCG基层人员长期疲劳
可信度

中高

+
+
+

透明化政策制度化

舆论趋势
+

PCG与NTF-WPS将现场影像、发言和事实核查结合,透明化从临时传播转向制度化工具。

+
驱动因素
  • 社交媒体传播
  • 国际支持需求
  • 事实核查需求
  • 对抗虚假信息
  • 国内民意
可能结果
  • 媒体随船常态化
  • 校园和公众教育增加
  • PCG发言人政治化
  • 社交平台叙事战加剧
风险
  • 个人化攻击
  • 舆论极化
  • 外交摩擦扩大
  • 误传未经核实信息
可信度

中高

+
+
+

福利与家属支持需求上升

人员生态趋势
+

长期前沿任务使家属、心理健康、医疗、休假和福利公平成为组织稳定性的关键变量。

+
驱动因素
  • 海上摩擦高频化
  • 人员长期远离家庭
  • 媒体英雄化
  • 伤病和风险暴露
  • 基金会项目扩展
可能结果
  • 更多心理健康项目
  • 家属支持制度化
  • 基层福利诉求增加
  • 组织保留人才压力上升
风险
  • 福利兑现不足导致信任下降
  • 家属隐私暴露
  • 英雄化遮蔽基层压力
可信度

中

+
+
+

护渔民生安全政治化

民生趋势
+

渔民安全和收入越来越成为PCG合法性、BFAR项目、地方政治和WPS主权叙事交汇点。

+
驱动因素
  • 黄岩岛和仙宾礁摩擦
  • 渔民补给项目
  • 渔民受伤事件
  • 媒体传播
  • 反军事化组织发声
可能结果
  • 更多护渔补给
  • 渔民成为政策象征
  • 民生补贴项目增加
  • 反军事化诉求持续
风险
  • 渔民被卷入大国竞争
  • 生计受禁航区影响
  • 民生诉求被政党化
可信度

中高

+
+
+

WPS议题选举化与政治极化

政治趋势
+

WPS立场影响候选人评价,PCG透明化和对华立场可能在2028年前继续被国内政治使用。

+
驱动因素
  • 民调支持强硬候选人
  • 参议院决议
  • PCG发言人争议
  • 中国使馆回应
  • 杜特尔特式缓和派与主权派分歧
可能结果
  • 候选人竞相表态
  • 透明化成为政治资产
  • 亲中/反美标签化
  • 外交议题内政化
风险
  • 政策讨论简化为站队
  • 专业海上执法被党争化
  • 虚假信息增加
可信度

中高

+
+

来源审查

点击详情查看用途、备注和链接

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
来源类型发布者日期可信度用于链接详情
Republic Act No. 9993 / Philippine Coast Guard Law of 2009法律文件Philippine Government / Official legal database2009
高
法律定位职责体系战时角色人员分类
打开
General Appropriations Act 2026 - DOTr / PCG预算文件Department of Budget and Management2026
高
预算福利项目拨款
打开
PCG personnel expansion public report菲律宾官方Philippine News Agency2025-01
中高
人员规模扩编趋势
打开
Reuters report on Second Thomas Shoal incident主流媒体Reuters2024-06
高
仁爱礁事件叙事对照
打开
Reuters report on BFAR near Scarborough Shoal主流媒体Reuters2024-02
高
黄岩岛护渔PCG发言
打开
Reuters report on Sabina Shoal deployment主流媒体Reuters2024-09
高
仙宾礁长期驻留
打开
Philippines unveils new coast guard command in Pag-asa Island主流媒体ABS-CBN2026-04
中高
Pag-asa海警区地方社会
打开
AP report on Philippine coast guard base in South China Sea主流媒体Associated Press2026-04
高
Pag-asa居民士气地方反馈
打开
The Guardian report on Thitu/Pag-asa fishing community主流媒体The Guardian2024-08
中高
居民规模基础设施儿童教育
打开
Reuters report on Pag-asa visit and residents主流媒体Reuters2026-02
高
Pag-asa居民心理通信环境渔民避让
打开
Kalayaan municipal official information菲律宾官方Kalayaan Municipality长期
中高
KalayaanPag-asa行政单元
打开
JICA Maritime Safety Capability Improvement Project Phase 3美国/盟友官方JICA2024-06
高
97米MRRV日本援助装备现代化
打开
JICA-PCG technical cooperation on maintenance and investigation美国/盟友官方JICA2025
高
维护SOP调查能力
打开
Purchase of 40 fast patrol crafts for PCG菲律宾官方Presidential Communications Office2024
高
法国快艇装备采购
打开
Philippine Coast Guard becomes H145 public mission operator主流媒体Airbus2018
高
航空资产搜救能力
打开
Australia drone and uncrewed maritime support to PCG美国/盟友官方Australian Embassy Philippines2025
高
无人机态势感知训练
打开
US Embassy and PCG training program菲律宾官方Philippine News Agency2025-10
中高
美国训练能力建设
打开
PCG joins US-Japan-Philippines trilateral coast guard exercise菲律宾官方Philippine News Agency2025-06
中高
三边海警演练
打开
Philippines and Vietnam first joint coast guard exercise主流媒体Reuters2024-08
高
越南合作区域海警网络
打开
KBBM Program empowers fisherfolk and reinforces PH presence in WPS菲律宾官方BFAR2025-06
高
渔民补贴BFAR护渔民生
打开
PCG resupplies fishermen in the South China Sea主流媒体USNI News2025-10
中高
护渔补给渔民
打开
Philippines says fishermen hurt near Sabina Shoal主流媒体Reuters2025-12
高
渔民受伤PCG救助
打开
APHR statement on Filipino fisherfolk and militarization基金会/公益组织ASEAN Parliamentarians for Human Rights2025
中
反军事化叙事渔民生计
打开
Philippine Coast Guard Support System Foundation基金会/公益组织PCG Foundation长期
中高
家属生态福利支持后方网络
打开
Strengthening the Nation through the Filipino Family基金会/公益组织PCG Foundation2025
中高
家庭日家庭叙事
打开
PCG Health and Wellness Caravan in CGDWV基金会/公益组织PCG Foundation2025-07
中高
心理健康压力管理福利
打开
Pulse Asia survey on transparency policy in WPS菲律宾官方Philippine Information Agency2025
中高
透明化政策民意
打开
SWS survey on candidates asserting PH rights in WPS主流媒体GMA Network2025
中高
选举政治WPS议题化
打开
West Philippine Sea Transparency Group菲律宾官方WPS Transparency Group2026
中高
透明化校园宣讲事实与合法性
打开
VERA Files West Philippine Sea topic事实核查机构VERA Files长期
高
事实核查错误叙事地图和法律概念
打开
Reuters investigation on China info influence in the Philippines主流媒体Reuters2025
高
信息战亲中/反美叙事
打开
Philippine Senate resolution related to WPS and Chinese Embassy remarks菲律宾官方Philippine News Agency2026-02
中高
参议院立场政治分歧
打开
Tarriela on transparency and WPS issues主流媒体Inquirer2026
中高
透明化政策2028选举记忆
打开
Philippines and US announce locations of four new EDCA sites美国/盟友官方U.S. Indo-Pacific Command2023
高
EDCA地方社会反美叙事
打开
Chinese official statements on South China Sea incidents中国官方China Coast Guard / MFA / Embassy public statements长期
中高
中国依法管控叙事叙事对照
—
+
+
+ + + + + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no27/no.27.html b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no27/no.27.html new file mode 100644 index 00000000..3bc5b9f6 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/templates/no27/no.27.html @@ -0,0 +1,98 @@ + + + + + +菲律宾“不做美国棋子”叙事专题数据库 v0.2 + + + +

专题数据库

+ +
+ + + + + \ No newline at end of file diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/services/position_sync.py b/offline-backend-20260512/backend/packages/harness/deerflow/services/position_sync.py index 2a6e86e8..23b4a8d9 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/services/position_sync.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/services/position_sync.py @@ -12,7 +12,8 @@ Resolution rule (per resource type): * For a resource type **with** bound tags → the union of resources carrying any of those tags. * For a resource type **without** bound tags → the *default set*: - - ``agent`` → all built-in agents (``user_id IS NULL``) + - ``agent`` -> built-ins first, then filtered out of + position-granted favorites - ``scheduled_task`` → empty (no built-in tasks) - ``skill`` → empty (the 广场 still shows every visible skill; a position only *adds* its tagged skills to the member's "我的技能") @@ -24,9 +25,12 @@ Resolution rule (per resource type): Materialization (``sync_user``) only ever touches rows it created itself (``origin='position'``), so a user's own favorites / subscriptions are never -clobbered. ``skill`` is materialized into ``skill_favorites`` exactly like -``agent`` → ``agent_favorites``: the tagged skills are *added* to the user's -"我的技能" while the browse-all 广场 stays complete (it is **not** restricted). +clobbered. ``agent`` keeps the original position-granted behavior for ordinary +tagged agents, but filters out built-ins and business-chain seats; those only +enter "mine" after an explicit user favorite. ``skill`` is still materialized +into ``skill_favorites`` so tagged skills are added to the user's "mine" +collection while the browse-all square stays complete (it is **not** +restricted). Only recommended-question visibility is computed live by :meth:`resolve_visible` and is *not* materialized. (The admin-configured prompts shown to users ARE the recommended questions — there is no separate prompt-template store.) @@ -34,6 +38,7 @@ recommended questions — there is no separate prompt-template store.) from __future__ import annotations +import json import logging import uuid from datetime import UTC, datetime @@ -43,13 +48,15 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from deerflow.persistence.agents.model import AgentFavoriteRow, AgentRow from deerflow.persistence.positions.base import PositionStore +from deerflow.persistence.roundtable_chains.model import RoundtableChainRow from deerflow.persistence.scheduled_tasks.model import ScheduledTaskSubscriptionRow from deerflow.persistence.skills.model import SkillFavoriteRow from deerflow.persistence.tags.base import TagStore logger = logging.getLogger(__name__) -# Materialized into "我的" tables by sync_user. +# Reconciled by sync_user. ``agent`` preserves ordinary position-granted rows, +# but filters built-ins and business-chain seats. _MATERIALIZED_TYPES = ("agent", "skill", "scheduled_task") # Resolved live at query time (visibility filters), never materialized. _VISIBILITY_TYPES = ("recommended_question",) @@ -93,8 +100,12 @@ class PositionSyncService: bindings = await self._positions.get_tag_bindings(pid) if pid else {} tag_ids = bindings.get(resource_type) or [] if tag_ids: - return set(await self._tags.list_targets_for_tags(resource_type, tag_ids)) - return await self._default_set(resource_type, session) + target = set(await self._tags.list_targets_for_tags(resource_type, tag_ids)) + else: + target = await self._default_set(resource_type, session) + if resource_type == "agent": + return await self._filter_materializable_agents(target, session) + return target async def _default_set(self, resource_type: str, session: AsyncSession) -> set[str]: if resource_type == "agent": @@ -103,6 +114,37 @@ class PositionSyncService: # scheduled_task (and any other materialized type) defaults to empty. return set() + async def _filter_materializable_agents(self, agent_ids: set[str], session: AsyncSession) -> set[str]: + if not agent_ids: + return set() + rows = ( + await session.execute( + select(AgentRow.id).where( + AgentRow.id.in_(agent_ids), + AgentRow.user_id.is_not(None), + ) + ) + ).all() + candidate_ids = {r[0] for r in rows} + return candidate_ids - await self._business_chain_agent_ids(session) + + async def _business_chain_agent_ids(self, session: AsyncSession) -> set[str]: + rows = (await session.execute(select(RoundtableChainRow.seats))).scalars() + agent_ids: set[str] = set() + for raw in rows: + if not raw: + continue + try: + seats = json.loads(raw) if isinstance(raw, str) else raw + except Exception: + continue + if not isinstance(seats, list): + continue + for seat in seats: + if isinstance(seat, dict) and isinstance(seat.get("agent_id"), str): + agent_ids.add(seat["agent_id"]) + return agent_ids + # --------------------------------------------------------- materialize async def sync_user(self, user_id: str) -> None: diff --git a/offline-backend-20260512/backend/tests/test_agents_backend.py b/offline-backend-20260512/backend/tests/test_agents_backend.py index 57345641..f87df609 100644 --- a/offline-backend-20260512/backend/tests/test_agents_backend.py +++ b/offline-backend-20260512/backend/tests/test_agents_backend.py @@ -1,33 +1,131 @@ -from __future__ import annotations - -import pytest -import yaml - -from app.gateway.routers.agents import ( - _backfill_listing_cache, - _load_config_extras, - _write_agent_config, -) -from app.gateway.services import build_run_config -from deerflow.config import paths as paths_module -from deerflow.config.agents_config import AgentConfig -from deerflow.persistence.agents import MemoryAgentStore - - +from __future__ import annotations + +import json +from uuid import uuid4 + +import pytest +import pytest_asyncio +import yaml +from sqlalchemy import select +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +from app.gateway.routers.agents import ( + _backfill_listing_cache, + _load_config_extras, + _write_agent_config, +) +from app.gateway.services import build_run_config +from deerflow.config import paths as paths_module +from deerflow.config.agents_config import AgentConfig +from deerflow.persistence.agents import AgentRepository, MemoryAgentStore +from deerflow.persistence.agents.model import AgentFavoriteRow, AgentRow +from deerflow.persistence.base import Base +from deerflow.persistence.positions.memory import MemoryPositionStore +from deerflow.persistence.roundtable_chains.model import RoundtableChainRow +from deerflow.persistence.tags.memory import MemoryTagStore +from deerflow.services.position_sync import PositionSyncService + + @pytest.fixture -def isolated_paths(tmp_path, monkeypatch): - """Redirect get_paths() at a fresh temp directory for the duration of one test.""" - monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path)) - monkeypatch.setattr(paths_module, "_paths", None) - yield tmp_path - monkeypatch.setattr(paths_module, "_paths", None) +def isolated_paths(tmp_path, monkeypatch): + """Redirect get_paths() at a fresh temp directory for the duration of one test.""" + monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path)) + monkeypatch.setattr(paths_module, "_paths", None) + yield tmp_path + monkeypatch.setattr(paths_module, "_paths", None) + + +@pytest_asyncio.fixture +async def sql_session_factory(tmp_path): + engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'agent-tests.db'}") + async with engine.begin() as conn: + # Importing PositionSyncService above registers the agent / skill / + # scheduled-task rows it materializes into. + await conn.run_sync(Base.metadata.create_all) + sf = async_sessionmaker(engine, expire_on_commit=False) + try: + yield sf + finally: + await engine.dispose() + + +async def _add_sql_agent(sf, *, agent_id: str, user_id: str | None) -> None: + async with sf() as session: + session.add( + AgentRow( + id=agent_id, + name=agent_id, + description="", + user_id=user_id, + published=True, + is_private=False, + square_id="", + ) + ) + await session.commit() + + +async def _add_sql_favorite(sf, *, user_id: str, agent_id: str, origin: str) -> None: + async with sf() as session: + session.add( + AgentFavoriteRow( + id=uuid4().hex, + user_id=user_id, + agent_id=agent_id, + origin=origin, + position_id="default" if origin == "position" else None, + ) + ) + await session.commit() + + +async def _add_sql_chain(sf, *, chain_id: str, agent_ids: list[str]) -> None: + async with sf() as session: + session.add( + RoundtableChainRow( + id=chain_id, + user_id="chain-owner", + title=chain_id, + description="", + seats=json.dumps( + [{"agent_id": agent_id, "name": agent_id, "description": ""} for agent_id in agent_ids], + ensure_ascii=False, + ), + ) + ) + await session.commit() + + +async def _sql_favorite_rows(sf, user_id: str) -> set[tuple[str, str]]: + async with sf() as session: + rows = ( + await session.execute( + select(AgentFavoriteRow.agent_id, AgentFavoriteRow.origin).where( + AgentFavoriteRow.user_id == user_id + ) + ) + ).all() + return {(agent_id, origin) for agent_id, origin in rows} + + +async def _default_positions() -> MemoryPositionStore: + positions = MemoryPositionStore() + await positions.create_position( + { + "id": "default", + "name": "Default", + "description": "", + "is_default": True, + } + ) + return positions @pytest.mark.asyncio -async def test_agent_store_allows_duplicate_chinese_names_and_filters_visibility() -> None: - store = MemoryAgentStore() - chinese_name = "\u4e2d\u6587\u52a9\u624b" - +async def test_agent_store_allows_duplicate_chinese_names_and_filters_visibility() -> None: + store = MemoryAgentStore() + chinese_name = "\u4e2d\u6587\u52a9\u624b" + await store.ensure_builtin({"id": "builtin-agent", "name": chinese_name, "description": "built in"}) await store.create({"id": "private-a", "name": chinese_name, "description": "private", "user_id": "user-a"}) await store.create({"id": "published-b", "name": chinese_name, "description": "published", "user_id": "user-b", "published": True}) @@ -35,14 +133,144 @@ async def test_agent_store_allows_duplicate_chinese_names_and_filters_visibility user_a_ids = {agent["id"] for agent in await store.list_visible("user-a")} assert user_a_ids == {"builtin-agent", "private-a", "published-b"} - user_c_ids = {agent["id"] for agent in await store.list_visible("user-c")} - assert user_c_ids == {"builtin-agent", "published-b"} - - -@pytest.mark.asyncio -async def test_agent_store_only_owner_can_update_or_delete() -> None: - store = MemoryAgentStore() - + user_c_ids = {agent["id"] for agent in await store.list_visible("user-c")} + assert user_c_ids == {"builtin-agent", "published-b"} + + +@pytest.mark.asyncio +async def test_position_sync_default_does_not_collect_builtin_agents(sql_session_factory) -> None: + await _add_sql_agent(sql_session_factory, agent_id="builtin-auto", user_id=None) + await _add_sql_agent(sql_session_factory, agent_id="builtin-manual", user_id=None) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="builtin-auto", + origin="position", + ) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="builtin-manual", + origin="user", + ) + + sync = PositionSyncService( + sql_session_factory, + await _default_positions(), + MemoryTagStore(), + ) + await sync.sync_user("user-a") + + assert await _sql_favorite_rows(sql_session_factory, "user-a") == { + ("builtin-manual", "user") + } + + +@pytest.mark.asyncio +async def test_position_sync_tagged_agents_are_not_auto_favorited(sql_session_factory) -> None: + await _add_sql_agent(sql_session_factory, agent_id="builtin-tagged", user_id=None) + await _add_sql_agent(sql_session_factory, agent_id="custom-tagged", user_id="owner") + await _add_sql_agent(sql_session_factory, agent_id="chain-tagged", user_id="owner") + await _add_sql_chain(sql_session_factory, chain_id="chain-1", agent_ids=["chain-tagged"]) + + positions = await _default_positions() + tags = MemoryTagStore() + await tags.create_tag({"id": "agent-tag", "name": "Agent tag", "tag_type": "agent"}) + await tags.set_target_tags("agent", "builtin-tagged", ["agent-tag"]) + await tags.set_target_tags("agent", "custom-tagged", ["agent-tag"]) + await tags.set_target_tags("agent", "chain-tagged", ["agent-tag"]) + await positions.set_tag_bindings("default", {"agent": ["agent-tag"]}) + + sync = PositionSyncService(sql_session_factory, positions, tags) + await sync.sync_user("user-a") + + assert await _sql_favorite_rows(sql_session_factory, "user-a") == { + ("custom-tagged", "position") + } + + +@pytest.mark.asyncio +async def test_mine_scope_ignores_auto_agent_favorites_but_keeps_manual_builtin(sql_session_factory) -> None: + await _add_sql_agent(sql_session_factory, agent_id="builtin-auto", user_id=None) + await _add_sql_agent(sql_session_factory, agent_id="builtin-manual", user_id=None) + await _add_sql_agent(sql_session_factory, agent_id="custom-auto", user_id="owner") + await _add_sql_agent(sql_session_factory, agent_id="chain-auto", user_id="owner") + await _add_sql_chain(sql_session_factory, chain_id="chain-1", agent_ids=["chain-auto"]) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="builtin-auto", + origin="position", + ) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="builtin-manual", + origin="user", + ) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="custom-auto", + origin="position", + ) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="chain-auto", + origin="position", + ) + + store = AgentRepository(sql_session_factory) + rows, total = await store.list_paginated( + user_id="user-a", + scope="mine", + search=None, + tag_ids=None, + page=1, + page_size=20, + ) + + assert total == 2 + assert {row["id"] for row in rows} == {"builtin-manual", "custom-auto"} + assert await store.list_favorite_ids("user-a") == {"builtin-manual", "custom-auto"} + + +@pytest.mark.asyncio +async def test_manual_favorite_upgrades_existing_auto_builtin_favorite(sql_session_factory) -> None: + await _add_sql_agent(sql_session_factory, agent_id="builtin-agent", user_id=None) + await _add_sql_favorite( + sql_session_factory, + user_id="user-a", + agent_id="builtin-agent", + origin="position", + ) + + store = AgentRepository(sql_session_factory) + assert await store.list_favorite_ids("user-a") == set() + + await store.add_favorite("user-a", "builtin-agent") + + assert await _sql_favorite_rows(sql_session_factory, "user-a") == { + ("builtin-agent", "user") + } + assert await store.list_favorite_ids("user-a") == {"builtin-agent"} + rows, total = await store.list_paginated( + user_id="user-a", + scope="mine", + search=None, + tag_ids=None, + page=1, + page_size=20, + ) + assert total == 1 + assert [row["id"] for row in rows] == ["builtin-agent"] + + +@pytest.mark.asyncio +async def test_agent_store_only_owner_can_update_or_delete() -> None: + store = MemoryAgentStore() + await store.ensure_builtin({"id": "builtin-agent", "name": "Built in"}) await store.create({"id": "private-a", "name": "Owned", "user_id": "user-a"}) diff --git a/offline-backend-20260512/backend/tests/test_ai_writing_knowledge_base.py b/offline-backend-20260512/backend/tests/test_ai_writing_knowledge_base.py index 01ed878f..2dadb6f3 100644 --- a/offline-backend-20260512/backend/tests/test_ai_writing_knowledge_base.py +++ b/offline-backend-20260512/backend/tests/test_ai_writing_knowledge_base.py @@ -10,7 +10,9 @@ from types import SimpleNamespace import pytest from deerflow.agents.ai_writing.nodes import researcher as R +from deerflow.agents.ai_writing.search import get_search_provider from deerflow.agents.ai_writing.search.base import SearchResult +from deerflow.agents.ai_writing.search.intranet_stub import IntranetSearchProvider class _FakeIntranet: @@ -21,6 +23,7 @@ class _FakeIntranet: self.raw_responses = [] self.calls = [] self.kw = kw + self.verify_ssl = kw.get("verify_ssl", False) _FakeIntranet.instances.append(self) async def search(self, query, max_results=5, **_): @@ -55,12 +58,24 @@ class _SkillProviderShouldNotRun: raise AssertionError("knowledge_base 模式不应调用通用 provider / 技能检索") -def _patch(monkeypatch, intranet_url="http://es/api", intranet_cls=_FakeIntranet): +class _FakeResponse: + status_code = 200 + text = '[{"title":"内网标题","content":"内网正文","url":"https://intranet/doc"}]' + + def raise_for_status(self) -> None: + return None + + def json(self): + return [{"title": "内网标题", "content": "内网正文", "url": "https://intranet/doc"}] + + +def _patch(monkeypatch, intranet_url="http://es/api", intranet_cls=_FakeIntranet, intranet_verify_ssl=False): _FakeIntranet.instances = [] cfg = { "max_materials": 20, "intranet_search_url": intranet_url, "intranet_knowledge_base": "samples", + "intranet_verify_ssl": intranet_verify_ssl, } monkeypatch.setattr(R, "get_ai_writing_config", lambda: cfg) monkeypatch.setattr(R, "use_builtin_agent", lambda *_: False) @@ -125,6 +140,15 @@ async def test_knowledge_base_no_url_falls_back_to_default_endpoint(monkeypatch) assert _FakeIntranet.instances[0].kw.get("base_url") == "", "空地址应原样下传,由 provider 回退默认端点" +@pytest.mark.asyncio +async def test_knowledge_base_passes_verify_ssl_config(monkeypatch): + _patch(monkeypatch, intranet_verify_ssl=True) + out = await R.researcher_node(_state(), {"configurable": {}}) + + assert _FakeIntranet.instances[0].kw.get("verify_ssl") is True + assert out["material_package"]["intranet_debug"]["verify_ssl"] is True + + @pytest.mark.asyncio async def test_knowledge_base_does_not_orchestrate_skills(monkeypatch): """knowledge_base 不编排技能:通用 provider 一旦被调用就会 raise。""" @@ -163,3 +187,44 @@ async def test_knowledge_base_failure_is_visible(monkeypatch): steps = pkg["skill_search_steps"] assert any(step["type"] == "skill_error" for step in steps) assert any(event["type"] == "notice" and event.get("raw") for event in out["progress_events"]) + + +@pytest.mark.asyncio +async def test_intranet_search_disables_ssl_verification_for_internal_https(monkeypatch): + import httpx + + seen: dict = {} + + class _FakeAsyncClient: + def __init__(self, **kwargs): + seen.update(kwargs) + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + async def post(self, endpoint, json): + seen["endpoint"] = endpoint + seen["payload"] = json + return _FakeResponse() + + monkeypatch.setattr(httpx, "AsyncClient", _FakeAsyncClient) + + provider = IntranetSearchProvider(base_url="https://intranet.example/search", knowledge_base_name="kb", timeout=300) + results = await provider.search("测试", max_results=1) + + assert results + assert seen["verify"] is False + assert seen["trust_env"] is False + assert seen["endpoint"] == "https://intranet.example/search" + assert seen["payload"]["knowledge_base_name"] == "kb" + assert provider.calls[0]["verify_ssl"] is False + + +def test_intranet_verify_ssl_can_be_reenabled_from_config(): + provider = get_search_provider({"search_provider": "intranet", "intranet_verify_ssl": "true"}) + + assert isinstance(provider, IntranetSearchProvider) + assert provider.verify_ssl is True