"description":"True only after verifying no more new unarchived articles; access failure is not exhaustion."
},
"exhaustion_reason":{
"type":"string"
},
"warnings":{
"type":"array",
"items":{
"type":"string"
}
},
"failures":{
"type":"array",
"items":{
"type":"object",
"properties":{
"type":{
"type":"string",
"enum":[
"PAYWALL",
"LOGIN_REQUIRED",
"CAPTCHA",
"EMPTY_BODY",
"BODY_TOO_SHORT",
"CONTENT_INCOMPLETE",
"FETCH_FAILED",
"ARCHIVE_FAILED",
"PUSH_FAILED",
"RUNTIME_ERROR",
"PERMISSION_DENIED",
"INVALID_OUTPUT",
"UNKNOWN"
]
},
"message":{
"type":"string"
},
"url":{
"type":"string"
}
},
"required":[
"type",
"message",
"url"
],
"additionalProperties":false
}
},
"articles":{
"type":"array",
"items":{
"type":"object",
"properties":{
"url":{
"type":"string"
},
"title":{
"type":"string"
},
"published_at":{
"type":"string"
},
"archive_path":{
"type":"string",
"description":"Absolute path of the archived original Markdown file; empty when not archived."
},
"status":{
"type":"string",
"enum":[
"SUCCESS",
"FAILED"
]
},
"failure_type":{
"type":[
"string",
"null"
],
"enum":[
null,
"PAYWALL",
"LOGIN_REQUIRED",
"CAPTCHA",
"EMPTY_BODY",
"BODY_TOO_SHORT",
"CONTENT_INCOMPLETE",
"FETCH_FAILED",
"ARCHIVE_FAILED",
"PUSH_FAILED",
"RUNTIME_ERROR",
"PERMISSION_DENIED",
"INVALID_OUTPUT",
"UNKNOWN"
]
},
"failure_reason":{
"type":[
"string",
"null"
],
"description":"Failure detail; null or empty for successful articles."
},
"quality_evidence":{
"type":"array",
"items":{
"type":"string"
},
"description":"Observed page access or truncation evidence; no invented explanations."
}
},
"required":[
"url",
"title",
"published_at",
"archive_path",
"status",
"failure_type",
"failure_reason",
"quality_evidence"
],
"additionalProperties":false
}
}
},
"required":[
"schema_version",
"source",
"requested_count",
"attempted_count",
"succeeded_count",
"failed_count",
"status",
"source_exhausted",
"exhaustion_reason",
"warnings",
"failures",
"articles"
],
"additionalProperties":false,
"description":"News Harvester v1 business result. Article content and properties are loaded locally from archive_path; counts and final status are recomputed locally."
<td><spanclass="badge {{ 'ok' if article.status == 'SUCCESS' else 'fail' }}">{{ '成功' if article.status == 'SUCCESS' else '失败' }}</span></td>
<td>{% if archive.get('body_length') is not none %}{{ archive.body_length }} 字符{% elif article.body_length is defined %}{{ article.body_length }} 字符{% else %}—{% endif %}</td>